Compare commits

...

75 Commits

Author SHA1 Message Date
Lucien Greathouse
def99a9e4d Update release job to fix 6.0.0 release 2020-03-29 17:09:52 -07:00
Lucien Greathouse
1214fc8b0d Release 6.0.0-rc.1
This change also includes some minor packaging changes in order to make Cargo happy.
2020-03-29 16:58:37 -07:00
Lucien Greathouse
5a5b1268d3 Update changelog 2020-03-29 16:03:58 -07:00
jeparlefrancais
6a1fffd1ce Infer class name (#210)
* infer service names

* Update project code and add support for StarterPlayer

* Store parent_class in InstigatingSource

* Update snapshots

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2020-03-29 16:03:15 -07:00
Lucien Greathouse
571ef3060a Update Changelog 2020-03-29 13:46:27 -07:00
jeparlefrancais
3cf82e112f Install plugin from CLI (#304)
* add install command

* cargo fmt

* filter spec files

* Update src/cli/plugin.rs

Co-Authored-By: Lucien Greathouse <me@lpghatguy.com>

* Update src/cli/plugin.rs

Co-Authored-By: Lucien Greathouse <me@lpghatguy.com>

* fix comments

* encode plugin with rbx_binary

* update build script

* refactor pathbuf error into io error

* fix rojo typo

* remove snafu

* Update `snapshot_from_fs_path`

* Print `rerun-if-changed` even for directories, in order to run the
  build.rs script when files are added.

* Switch `filter_map` loop to a regular for loop. I like the FP-style
  iterator stuff in Rust, but I think Result handling is easier in a
  normal loop. Also, I don't believe the result of read_dir implements
  `ExactSizedIterator`, so some of the wins of map+collect aren't there.

* Replace Result::unwrap with ? in build.rs

* Simplify error handling code in runtime

* Checkout with submodules

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2020-03-29 13:41:54 -07:00
Lucien Greathouse
9b459c20d6 Fix GHA only running on pushes to master 2020-03-29 13:17:06 -07:00
Lucien Greathouse
5c85cd27c3 Fix default place project.
Closes #311.
2020-03-29 12:21:55 -07:00
Lucien Greathouse
4bf73c7a8a Implement support for turning .json files into Lua modules (#308)
* Stub implementation

* Flesh out feature and add tests. Other snapshots currently failing.

* Blacklist .meta.json in JSON handler

* Write to correct property (Source) instead of Value

* Update changelog
2020-03-28 00:36:01 -07:00
Lucien Greathouse
62e51b7535 Upgrade to latest rbx-dom 2020-03-27 23:58:31 -07:00
Lucien Greathouse
729a7f0053 Turn panics into errors in ServeSession 2020-03-26 12:16:55 -07:00
Lucien Greathouse
03c297190d Make ServeSession::new fallible 2020-03-26 12:07:44 -07:00
Lucien Greathouse
9c790eddd7 Tidy up ServeSession now that trait bounds are gone 2020-03-26 12:06:16 -07:00
Lucien Greathouse
8ebe7e332b Update Changelog 2020-03-25 17:02:32 -07:00
Lucien Greathouse
f43777e37e Require a Rojo project again (#307) 2020-03-25 17:01:28 -07:00
Lucien Greathouse
691a8fcdeb Upgrade lockfile using latest rustc 2020-03-25 16:15:21 -07:00
Lucien Greathouse
69c0e8d70e Fix warnings 2020-03-21 17:49:56 -07:00
Lucien Greathouse
330c92c9a8 Refactor ChangeProcessor loop to get rid of panics 2020-03-21 17:47:25 -07:00
Lucien Greathouse
cf0ff60d31 plugin: Add simple signal implementation for future work 2020-03-18 23:31:22 -07:00
Lucien Greathouse
9e9cf5dd1f plugin: Add support for pausing updates tracked by InstanceMap 2020-03-18 23:27:30 -07:00
Lucien Greathouse
5768d8e4a4 plugin: Miscellaneous cleanup 2020-03-18 23:15:03 -07:00
Lucien Greathouse
3b433e53be Memofs v0.1.1 2020-03-18 18:35:44 -07:00
Lucien Greathouse
28ddf40344 memofs: Update fs-err and use it more 2020-03-18 18:06:58 -07:00
Lucien Greathouse
c1286db9c1 Update Changelog 2020-03-18 16:26:41 -07:00
Lucien Greathouse
f13940262e Update CHANGELOG 2020-03-18 12:03:50 -07:00
Lucien Greathouse
9f0a6101b8 Add configurable color options 2020-03-18 12:03:07 -07:00
Lucien Greathouse
0b0fe01a7c Tidy up root repository files 2020-03-18 11:40:12 -07:00
Lucien Greathouse
85e098d5c8 Update README 2020-03-18 11:36:50 -07:00
Lucien Greathouse
e8d1faf4e2 Update changelog 2020-03-18 10:43:42 -07:00
Lucien Greathouse
2a46df1110 Expose two-way sync.
- Convert plugin DevSettings flag to settings panel feature
- Remove server feature, always enable write API
2020-03-18 10:39:40 -07:00
Lucien Greathouse
1601e6d26e Update changelog 2020-03-17 23:20:40 -07:00
Lucien Greathouse
0e4f6dea2b plugin: Add setting for opening scripts externally 2020-03-17 23:20:05 -07:00
Lucien Greathouse
a2356773dc Add checkbox and fill out settings panel 2020-03-17 23:14:32 -07:00
Lucien Greathouse
4a4da4737d Fix plugin settings persistent 2020-03-17 23:03:59 -07:00
Lucien Greathouse
2cefd1bf2e plugin: Add PluginSettings context item, render it in settings screen 2020-03-17 23:03:01 -07:00
Lucien Greathouse
c5ce15fe34 plugin: Add dummy settings panel 2020-03-17 22:38:53 -07:00
Lucien Greathouse
76dea568c9 Update changelog 2020-03-17 22:30:00 -07:00
Lucien Greathouse
8e81140eff Increase verbosity of logging 2020-03-17 22:29:23 -07:00
Lucien Greathouse
d58e1f0792 Add logging when running rojo build 2020-03-17 22:28:38 -07:00
Lucien Greathouse
830c242751 plugin: Stop using codename in dev mode 2020-03-17 22:25:04 -07:00
Lucien Greathouse
91d45afd0f Add plugin feature 'UnstableOpenScriptsExternally' 2020-03-17 18:13:52 -07:00
Lucien Greathouse
102c77b23e Implement /api/open/{id} to open a script by ID in your default editor 2020-03-17 17:50:54 -07:00
Lucien Greathouse
aa4039a2e7 bye snafu 2020-03-16 23:37:00 -07:00
Lucien Greathouse
c065ded440 Tidy up SnapshotError a lot 2020-03-16 21:35:46 -07:00
Lucien Greathouse
f69096dadb Use thiserror and anyhow for command-level error types 2020-03-16 21:13:38 -07:00
Lucien Greathouse
363f95ba14 memofs: Use fs_err instead of std::fs when possible 2020-03-16 20:35:58 -07:00
Lucien Greathouse
dcc15e8911 Refactor upload to use ServeSession and drop common_setup 2020-03-16 20:20:12 -07:00
Lucien Greathouse
bd13047b58 Release 0.6.0-alpha.3 2020-03-13 23:50:54 -07:00
Lucien Greathouse
1a83789c01 Update Changelog 2020-03-13 20:50:03 -07:00
Lucien Greathouse
1cbe272e19 Fix malformed meta files causing panics.
Fixes #280.
2020-03-13 20:38:06 -07:00
Lucien Greathouse
6de74b41b3 Update snapshot error type and handle serde_json errors in JSON models 2020-03-13 20:31:31 -07:00
Lucien Greathouse
b0fc9ee507 Rename 'src/common' in default place template to 'src/shared' 2020-03-13 20:25:50 -07:00
Lucien Greathouse
a95ffe1d31 Add snapshot error handling to ChangeProcessor 2020-03-13 20:24:14 -07:00
Lucien Greathouse
4119a510f5 Fix broken error source tracking 2020-03-13 20:12:42 -07:00
Lucien Greathouse
cfa7f03815 Add help link to default model project 2020-03-13 20:11:08 -07:00
Lucien Greathouse
9b4c89820d Fix extra Git output in rojo init and update place template 2020-03-13 20:10:31 -07:00
Lucien Greathouse
fe874720aa Upgrade rojo init to create README.md and create Git repo 2020-03-13 20:00:48 -07:00
Lucien Greathouse
f7c0f33eb5 Add brand new rojo init command 2020-03-13 19:33:43 -07:00
Lucien Greathouse
c1bf9d9dfc Update duplicate env_logger dep 2020-03-12 19:39:27 -07:00
Lucien Greathouse
255bf439d3 Add 'rojo doc' command 2020-03-12 16:02:19 -07:00
Lucien Greathouse
2a31937b81 Use CARGO_PKG_REPOSITORY instead of hard-coded URL 2020-03-12 15:48:30 -07:00
Lucien Greathouse
eb8964e1d1 Improve panic messaging and process behavior.
- Replaced main() to use custom panic hook
- Updated log formatting
- Switched to panic=abort, since we don't need to unwind now.
- Process will now abort if any thread panics.
2020-03-12 15:42:56 -07:00
Lucien Greathouse
fe0ca280a1 Update disabled CSV middleware test 2020-03-10 23:52:55 -07:00
Lucien Greathouse
e8e3b7b985 plugin: Fix dark theme when resized to be wide 2020-03-10 20:34:12 -07:00
Lucien Greathouse
c437507442 Update changelogs 2020-03-10 18:15:06 -07:00
Lucien Greathouse
ca151b434e Update memofs dependency 2020-03-10 18:13:34 -07:00
Lucien Greathouse
10ba74c21e memofs: Add description 2020-03-10 18:11:39 -07:00
Lucien Greathouse
6191b6371d Update using cargo-readme 2020-03-10 18:09:04 -07:00
Lucien Greathouse
5be4175ac3 Rename vfs -> memofs across the codebase 2020-03-10 18:05:31 -07:00
Lucien Greathouse
f61f3671a6 Choose 'memofs' as the vfs name 2020-03-10 17:57:57 -07:00
Lucien Greathouse
477e0ada32 VFS in external crate (#297)
* vroom

* Port dir middleware

* Filter rules

* Directory metadata

* Project support

* Enable Lua support

* StringValue support

* CSV

* rbxm, rbxmx, and rbxlx

* JSON models

* Clean up some warnings

* Strip out PathMap

* Unwatch paths when they're reported as removed

* Fix 'rojo upload' behavior

* Upgrade to Insta 0.13.1

* Update dependencies

* Release 0.6.0-alpha.2

* Fix bad merge

* Replace MemoryBackend with InMemoryFs

* Sledgehammer tests into passing for now

* Txt middleware

* Update easy snapshot tests

* Lua tests

* Project middleware tests

* Try to fix test failures by sorting

* Port first set of serve session tests

* Add InMemoryFs::raise_event

* Finish porting serve session tests

* Remove UI code for introspecting VFS for now

* VFS docs
2020-03-10 17:38:49 -07:00
Lucien Greathouse
a884f693ae Update changelog 2020-03-08 18:34:11 -07:00
Lucien Greathouse
3107b1b21b Dynamic theming. Closes #241.
Upgrades to Roact master and introduces dynamic theme switching.

We branch on the theme name in order to try to use Rojo's brand
colors instead of Studio's. I kind of winged it with these colors
and we might want to choose slightly nicer dark theme colors
in the future.

I also took the opportunity to reorganize the color naming scheme
since it didn't really work for dark theme.
2020-03-08 18:32:42 -07:00
Lucien Greathouse
04529de7b3 Update Changelog 2020-03-08 17:48:50 -07:00
Lucien Greathouse
199a39208c Implement 'rojo build --watch' (#284)
* Refactor build command to reproduce model more easily

* Spawn ServeSession for building
2020-03-08 17:48:14 -07:00
164 changed files with 24431 additions and 5000 deletions

View File

@@ -1,6 +1,9 @@
name: CI name: CI
on: [push] on:
pull_request:
push:
branches: ["*"]
jobs: jobs:
build: build:
@@ -13,6 +16,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
with:
submodules: true
- name: Setup Rust toolchain - name: Setup Rust toolchain
run: rustup default ${{ matrix.rust_version }} run: rustup default ${{ matrix.rust_version }}

View File

@@ -10,6 +10,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
with:
submodules: true
- name: Build release binary - name: Build release binary
run: cargo build --verbose --locked --release run: cargo build --verbose --locked --release
@@ -25,6 +27,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
with:
submodules: true
- name: Install Rust - name: Install Rust
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
@@ -47,6 +51,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
with:
submodules: true
- name: Build - name: Build
run: cargo build --locked --verbose --release run: cargo build --locked --verbose --release

5
.gitmodules vendored
View File

@@ -9,7 +9,4 @@
url = https://github.com/LPGhatguy/roblox-lua-promise.git url = https://github.com/LPGhatguy/roblox-lua-promise.git
[submodule "plugin/modules/t"] [submodule "plugin/modules/t"]
path = plugin/modules/t path = plugin/modules/t
url = https://github.com/osyrisrblx/t.git url = https://github.com/osyrisrblx/t.git
[submodule "plugin/modules/rbx-dom"]
path = plugin/modules/rbx-dom
url = http://github.com/rojo-rbx/rbx-dom

View File

@@ -1,6 +1,32 @@
# Rojo Changelog # Rojo Changelog
## Unreleased Changes for 0.6.x ## Unreleased Changes
## [6.0.0 Release Candidate 1](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0-rc.1) (March 29, 2020)
This release jumped from 0.6.0 to 6.0.0. Rojo has been in use in production for many users for quite a long times, and so 6.0 is a more accurate reflection of Rojo's version than a pre-1.0 version.
* Added basic settings panel to plugin, with two settings:
* "Open Scripts Externally": When enabled, opening a script in Studio will instead open it in your default text editor.
* "Two-Way Sync": When enabled, Rojo will attempt to save changes to your place back to the filesystem. **Very early feature, very broken, beware!**
* Added `--color` option to force-enable or force-disable color in Rojo's output.
* Added support for turning `.json` files into `ModuleScript` instances ([#308](https://github.com/rojo-rbx/rojo/pull/308))
* Added `rojo plugin install` and `rojo plugin uninstall` to allow Rojo to manage its Roblox Studio plugin. ([#304](https://github.com/rojo-rbx/rojo/pull/304))
* Class names no longer need to be specified for Roblox services in Rojo projects. ([#210](https://github.com/rojo-rbx/rojo/pull/210))
* The server half of **experimental** two-way sync is now enabled by default.
* Increased default logging verbosity in commands like `rojo build`.
* Rojo now requires a project file again, just like 0.5.4.
## [0.6.0 Alpha 3](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.3) (March 13, 2020)
* Added `--watch` argument to `rojo build`. ([#284](https://github.com/rojo-rbx/rojo/pull/284))
* Added dark theme support to plugin. ([#241](https://github.com/rojo-rbx/rojo/issues/241))
* Added a revamped `rojo init` command, which will now create more complete projects.
* Added the `rojo doc` command, which opens Rojo's documentation in your browser.
* Fixed many crashes from malformed projects and filesystem edge cases in `rojo serve`.
* Simplified filesystem access code dramatically.
* Improved error reporting and logging across the board.
* Log messages have a less noisy prefix.
* Any thread panicking now causes Rojo to abort instead of existing as a zombie.
* Errors now have a list of causes, helping make many errors more clear.
## [0.6.0 Alpha 2](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.2) (March 6, 2020) ## [0.6.0 Alpha 2](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.2) (March 6, 2020)
* Fixed `rojo upload` command always uploading models. * Fixed `rojo upload` command always uploading models.

View File

@@ -33,15 +33,13 @@ Please file issues and we'll try to help figure out what the best way forward is
## Pushing a Rojo Release ## Pushing a Rojo Release
The Rojo release process is pretty manual right now. If you need to do it, here's how: The Rojo release process is pretty manual right now. If you need to do it, here's how:
1. Bump server version in [`server/Cargo.toml`](server/Cargo.toml) 1. Bump server version in [`Cargo.toml`](Cargo.toml)
2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua) 2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua)
3. Run `cargo test` to update `Cargo.lock` and double-check tests 3. Run `cargo test` to update `Cargo.lock` and double-check tests
4. Update [`CHANGELOG.md`](CHANGELOG.md) 4. Update [`CHANGELOG.md`](CHANGELOG.md)
5. Commit! 5. Commit!
* `git add . && git commit -m "Release vX.Y.Z"` * `git add . && git commit -m "Release vX.Y.Z"`
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13` 6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
7. Build Windows release build of CLI
* `cargo build --release`
7. Publish the CLI 7. Publish the CLI
* `cargo publish` * `cargo publish`
8. Build and upload the plugin 8. Build and upload the plugin
@@ -52,4 +50,5 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
10. Copy GitHub release content from previous release 10. Copy GitHub release content from previous release
* Update the leading text with a summary about the release * Update the leading text with a summary about the release
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md) * Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
* Write a small summary of each major feature * Write a small summary of each major feature
* Attach release artifacts from GitHub Actions for each platform

2261
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rojo" name = "rojo"
version = "0.6.0-alpha.2" version = "6.0.0-rc.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"
@@ -11,19 +11,21 @@ readme = "README.md"
edition = "2018" edition = "2018"
exclude = [ exclude = [
"/plugin/**",
"/test-projects/**", "/test-projects/**",
] ]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
[features] [features]
default = [] default = []
# Turn on support for specifying glob ignore path rules in the project format. # Turn on support for specifying glob ignore path rules in the project format.
unstable_glob_ignore_paths = [] unstable_glob_ignore_paths = []
# Turn on the server half of Rojo's unstable two-way sync feature.
unstable_two_way_sync = []
# Enable this feature to live-reload assets from the web UI. # Enable this feature to live-reload assets from the web UI.
dev_live_assets = [] dev_live_assets = []
@@ -32,14 +34,14 @@ members = [
"rojo-test", "rojo-test",
"rojo-insta-ext", "rojo-insta-ext",
"clibrojo", "clibrojo",
"vfs", "memofs",
] ]
default-members = [ default-members = [
".", ".",
"rojo-test", "rojo-test",
"rojo-insta-ext", "rojo-insta-ext",
"vfs", "memofs",
] ]
[lib] [lib]
@@ -55,9 +57,15 @@ name = "build"
harness = false harness = false
[dependencies] [dependencies]
memofs = { version = "0.1.2", path = "memofs" }
anyhow = "1.0.27"
backtrace = "0.3"
bincode = "1.2.1"
crossbeam-channel = "0.4.0" crossbeam-channel = "0.4.0"
csv = "1.1.1" csv = "1.1.1"
env_logger = "0.7.1" env_logger = "0.7.1"
fs-err = "2.2.0"
futures = "0.1.29" futures = "0.1.29"
globset = "0.4.4" globset = "0.4.4"
humantime = "1.3.0" humantime = "1.3.0"
@@ -67,6 +75,7 @@ lazy_static = "1.4.0"
log = "0.4.8" log = "0.4.8"
maplit = "1.0.1" maplit = "1.0.1"
notify = "4.0.14" notify = "4.0.14"
opener = "0.4.1"
rbx_binary = "0.5.0" rbx_binary = "0.5.0"
rbx_dom_weak = "1.10.1" rbx_dom_weak = "1.10.1"
rbx_reflection = "3.3.408" rbx_reflection = "3.3.408"
@@ -75,16 +84,26 @@ regex = "1.3.1"
reqwest = "0.9.20" reqwest = "0.9.20"
ritz = "0.1.0" ritz = "0.1.0"
rlua = "0.17.0" rlua = "0.17.0"
roblox_install = "0.2.2"
serde = { version = "1.0", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0" serde_json = "1.0"
snafu = "0.6.0"
structopt = "0.3.5" structopt = "0.3.5"
termcolor = "1.0.5" termcolor = "1.0.5"
thiserror = "1.0.11"
tokio = "0.1.22"
uuid = { version = "0.8.1", features = ["v4", "serde"] } uuid = { version = "0.8.1", features = ["v4", "serde"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.6.2" winreg = "0.6.2"
[build-dependencies]
memofs = { version = "0.1.0", path = "memofs" }
anyhow = "1.0.27"
bincode = "1.2.1"
fs-err = "2.3.0"
maplit = "1.0.1"
[dev-dependencies] [dev-dependencies]
rojo-insta-ext = { path = "rojo-insta-ext" } rojo-insta-ext = { path = "rojo-insta-ext" }
@@ -95,5 +114,4 @@ paste = "0.1"
pretty_assertions = "0.6.1" pretty_assertions = "0.6.1"
serde_yaml = "0.8.9" serde_yaml = "0.8.9"
tempfile = "3.0" tempfile = "3.0"
tokio = "0.1.22"
walkdir = "2.1" walkdir = "2.1"

View File

@@ -13,8 +13,8 @@
<a href="https://crates.io/crates/rojo"> <a href="https://crates.io/crates/rojo">
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" /> <img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
</a> </a>
<a href="https://rojo.space/docs/0.5.x"> <a href="https://rojo.space/docs">
<img src="https://img.shields.io/badge/docs-0.5.x-brightgreen.svg" alt="Rojo 0.5.x Documentation" /> <img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" />
</a> </a>
</div> </div>
@@ -34,11 +34,10 @@ Rojo enables:
* Streaming `rbxmx` and `rbxm` models into your game in real time * Streaming `rbxmx` and `rbxm` models into your game in real time
* Packaging and deploying your project to Roblox.com from the command line * Packaging and deploying your project to Roblox.com from the command line
Soon, Rojo will be able to: In the future, Rojo will be able to:
* Automatically convert your existing game to work with Rojo
* Sync instances from Roblox Studio to the filesystem * Sync instances from Roblox Studio to the filesystem
* Automatically manage your assets on Roblox.com, like images and sounds * Automatically convert your existing game to work with Rojo
* Import custom instances like MoonScript code * Import custom instances like MoonScript code
## [Documentation](https://rojo.space/docs) ## [Documentation](https://rojo.space/docs)

View File

@@ -0,0 +1,11 @@
# {project_name}
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started
To build this library or plugin, use:
```bash
rojo build -o "{project_name}.rbxmx"
```
For more help, check out [the Rojo documentation](https://rojo.space/docs).

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
# {project_name}
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started
To build the place from scratch, use:
```bash
rojo build -o "{project_name}.rbxlx"
```
Next, open `{project_name}.rbxlx` in Roblox Studio and start the Rojo server:
```bash
rojo serve
```
For more help, check out [the Rojo documentation](https://rojo.space/docs).

View File

@@ -1,39 +1,36 @@
{ {
"name": "[placeholder]", "name": "{project_name}",
"tree": { "tree": {
"$className": "DataModel", "$className": "DataModel",
"HttpService": {
"$className": "HttpService",
"$properties": {
"HttpEnabled": true
}
},
"Lighting": {
"$className": "Lighting",
"$properties": {
"Ambient": [
0,
0,
0
],
"Brightness": 2,
"GlobalShadows": true,
"Outlines": false,
"Technology": "Voxel"
}
},
"ReplicatedStorage": { "ReplicatedStorage": {
"$className": "ReplicatedStorage", "$className": "ReplicatedStorage",
"Source": {
"$path": "src" "Common": {
"$path": "src/shared"
} }
}, },
"SoundService": {
"$className": "SoundService", "ServerScriptService": {
"$properties": { "$className": "ServerScriptService",
"RespectFilteringEnabled": true
"Server": {
"$path": "src/server"
} }
}, },
"StarterPlayer": {
"$className": "StarterPlayer",
"StarterPlayerScripts": {
"$className": "StarterPlayerScripts",
"Client": {
"$path": "src/client"
}
}
},
"Workspace": { "Workspace": {
"$className": "Workspace", "$className": "Workspace",
"$properties": { "$properties": {
@@ -61,6 +58,32 @@
] ]
} }
} }
},
"Lighting": {
"$className": "Lighting",
"$properties": {
"Ambient": [
0,
0,
0
],
"Brightness": 2,
"GlobalShadows": true,
"Outlines": false,
"Technology": "Voxel"
}
},
"SoundService": {
"$className": "SoundService",
"$properties": {
"RespectFilteringEnabled": true
}
},
"HttpService": {
"$className": "HttpService",
"$properties": {
"HttpEnabled": true
}
} }
} }
} }

View File

@@ -0,0 +1,6 @@
# Project place file
/{project_name}.rbxlx
# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock

74
build.rs Normal file
View File

@@ -0,0 +1,74 @@
use std::{
env, io,
path::{Path, PathBuf},
};
use fs_err as fs;
use fs_err::File;
use maplit::hashmap;
use memofs::VfsSnapshot;
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
println!("cargo:rerun-if-changed={}", path.display());
if path.is_dir() {
let mut children = Vec::new();
for entry in fs::read_dir(path)? {
let entry = entry?;
let file_name = entry.file_name().to_str().unwrap().to_owned();
// We can skip any TestEZ test files since they aren't necessary for
// the plugin to run.
if file_name.ends_with(".spec.lua") {
continue;
}
let child_snapshot = snapshot_from_fs_path(&entry.path())?;
children.push((file_name, child_snapshot));
}
Ok(VfsSnapshot::dir(children))
} else {
let content = fs::read_to_string(path)?;
Ok(VfsSnapshot::file(content))
}
}
fn main() -> Result<(), anyhow::Error> {
let out_dir = env::var_os("OUT_DIR").unwrap();
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
let plugin_root = PathBuf::from(root_dir).join("plugin");
let plugin_modules = plugin_root.join("modules");
let snapshot = VfsSnapshot::dir(hashmap! {
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
"http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
"modules" => VfsSnapshot::dir(hashmap! {
"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"))?
}),
}),
});
let out_path = Path::new(&out_dir).join("plugin.bincode");
let out_file = File::create(&out_path)?;
bincode::serialize_into(out_file, &snapshot)?;
Ok(())
}

View File

@@ -1,30 +0,0 @@
digraph Rojo {
concentrate = true;
node [fontname = "sans-serif"];
plugin [label="Roblox Studio Plugin"]
session [label="Session"]
rbx_tree [label="Instance Tree"]
imfs [label="In-Memory Filesystem"]
fs_impl [label="Filesystem Implementation\n(stubbed in tests)"]
fs [label="Real Filesystem"]
snapshot_subsystem [label="Snapshot Subsystem\n(reconciler)"]
snapshot_generator [label="Snapshot Generator"]
user_middleware [label="User Middleware\n(MoonScript, etc.)"]
builtin_middleware [label="Built-in Middleware\n(.lua, .rbxm, etc.)"]
api [label="Web API"]
file_watcher [label="File Watcher"]
session -> imfs
session -> rbx_tree
session -> snapshot_subsystem
session -> snapshot_generator
session -> file_watcher [dir="both"]
file_watcher -> imfs
snapshot_generator -> user_middleware
snapshot_generator -> builtin_middleware
plugin -> api [style="dotted"; dir="both"; minlen=2]
api -> session
imfs -> fs_impl
fs_impl -> fs
}

9
memofs/CHANGELOG.md Normal file
View File

@@ -0,0 +1,9 @@
# memofs Changelog
## Unreleased Changes
## 0.1.1 (2020-03-18)
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
## 0.1.0 (2020-03-10)
* Initial release

View File

@@ -1,14 +1,17 @@
[package] [package]
name = "vfs" name = "memofs"
version = "0.1.0" description = "Virtual filesystem with configurable backends."
version = "0.1.2"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
edition = "2018" edition = "2018"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
homepage = "https://github.com/rojo-rbx/rojo" homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
notify = "4.0.15"
crossbeam-channel = "0.4.0" crossbeam-channel = "0.4.0"
fs-err = "2.3.0"
notify = "4.0.15"
serde = { version = "1.0", features = ["derive"] }

22
memofs/README.md Normal file
View File

@@ -0,0 +1,22 @@
# memofs
[![Crates.io](https://img.shields.io/crates/v/memofs.svg)](https://crates.io/crates/memofs)
Implementation of a virtual filesystem with a configurable backend and file
watching.
memofs is currently an unstable minimum viable library. Its primary consumer is
[Rojo](https://github.com/rojo-rbx/rojo), a build system for Roblox.
### Current Features
* API similar to `std::fs`
* Configurable backends
* `StdBackend`, which uses `std::fs` and the `notify` crate
* `NoopBackend`, which always throws errors
* `InMemoryFs`, a simple in-memory filesystem useful for testing
### Future Features
* Hash-based hierarchical memoization keys (hence the name)
* Configurable caching (write-through, write-around, write-back)
## License
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.

7
memofs/README.tpl Normal file
View File

@@ -0,0 +1,7 @@
# {{crate}}
[![Crates.io](https://img.shields.io/crates/v/memofs.svg)](https://crates.io/crates/memofs)
{{readme}}
## License
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.

View File

@@ -1,32 +1,77 @@
use std::collections::{BTreeSet, HashMap, VecDeque}; use std::collections::{BTreeSet, HashMap, VecDeque};
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crossbeam_channel::{Receiver, Sender};
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent, VfsSnapshot}; use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent, VfsSnapshot};
/// `VfsBackend` that reads from an in-memory filesystem, intended for setting /// In-memory filesystem that can be used as a VFS backend.
/// up testing scenarios quickly. ///
#[derive(Debug)] /// Internally reference counted to enable giving a copy to
pub struct MemoryBackend { /// [`Vfs`](struct.Vfs.html) and keeping the original to mutate the filesystem's
entries: HashMap<PathBuf, Entry>, /// state with.
orphans: BTreeSet<PathBuf>, #[derive(Debug, Clone)]
pub struct InMemoryFs {
inner: Arc<Mutex<InMemoryFsInner>>,
} }
impl MemoryBackend { impl InMemoryFs {
/// Create a new empty `InMemoryFs`.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
entries: HashMap::new(), inner: Arc::new(Mutex::new(InMemoryFsInner::new())),
orphans: BTreeSet::new(),
} }
} }
/// Load a [`VfsSnapshot`](enum.VfsSnapshot.html) into a subtree of the
/// in-memory filesystem.
///
/// This function will return an error if the operations required to apply
/// the snapshot result in errors, like trying to create a file inside a
/// file.
pub fn load_snapshot<P: Into<PathBuf>>( pub fn load_snapshot<P: Into<PathBuf>>(
&mut self, &mut self,
path: P, path: P,
snapshot: VfsSnapshot, snapshot: VfsSnapshot,
) -> io::Result<()> { ) -> io::Result<()> {
let path = path.into(); let mut inner = self.inner.lock().unwrap();
inner.load_snapshot(path.into(), snapshot)
}
/// Raises a filesystem change event.
///
/// If this `InMemoryFs` is being used as the backend of a
/// [`Vfs`](struct.Vfs.html), then any listeners be notified of this event.
pub fn raise_event(&mut self, event: VfsEvent) {
let inner = self.inner.lock().unwrap();
inner.event_sender.send(event).unwrap();
}
}
#[derive(Debug)]
struct InMemoryFsInner {
entries: HashMap<PathBuf, Entry>,
orphans: BTreeSet<PathBuf>,
event_receiver: Receiver<VfsEvent>,
event_sender: Sender<VfsEvent>,
}
impl InMemoryFsInner {
fn new() -> Self {
let (event_sender, event_receiver) = crossbeam_channel::unbounded();
Self {
entries: HashMap::new(),
orphans: BTreeSet::new(),
event_receiver,
event_sender,
}
}
fn load_snapshot(&mut self, path: PathBuf, snapshot: VfsSnapshot) -> io::Result<()> {
if let Some(parent_path) = path.parent() { if let Some(parent_path) = path.parent() {
if let Some(parent_entry) = self.entries.get_mut(parent_path) { if let Some(parent_entry) = self.entries.get_mut(parent_path) {
if let Entry::Dir { children } = parent_entry { if let Entry::Dir { children } = parent_entry {
@@ -84,9 +129,11 @@ enum Entry {
Dir { children: BTreeSet<PathBuf> }, Dir { children: BTreeSet<PathBuf> },
} }
impl VfsBackend for MemoryBackend { impl VfsBackend for InMemoryFs {
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> { fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
match self.entries.get(path) { let inner = self.inner.lock().unwrap();
match inner.entries.get(path) {
Some(Entry::File { contents }) => Ok(contents.clone()), Some(Entry::File { contents }) => Ok(contents.clone()),
Some(Entry::Dir { .. }) => must_be_file(path), Some(Entry::Dir { .. }) => must_be_file(path),
None => not_found(path), None => not_found(path),
@@ -94,8 +141,10 @@ impl VfsBackend for MemoryBackend {
} }
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> { fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
self.load_snapshot( let mut inner = self.inner.lock().unwrap();
path,
inner.load_snapshot(
path.to_path_buf(),
VfsSnapshot::File { VfsSnapshot::File {
contents: data.to_owned(), contents: data.to_owned(),
}, },
@@ -103,7 +152,9 @@ impl VfsBackend for MemoryBackend {
} }
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
match self.entries.get(path) { let inner = self.inner.lock().unwrap();
match inner.entries.get(path) {
Some(Entry::Dir { children }) => { Some(Entry::Dir { children }) => {
let iter = children let iter = children
.clone() .clone()
@@ -120,9 +171,11 @@ impl VfsBackend for MemoryBackend {
} }
fn remove_file(&mut self, path: &Path) -> io::Result<()> { fn remove_file(&mut self, path: &Path) -> io::Result<()> {
match self.entries.get(path) { let mut inner = self.inner.lock().unwrap();
match inner.entries.get(path) {
Some(Entry::File { .. }) => { Some(Entry::File { .. }) => {
self.remove(path.to_owned()); inner.remove(path.to_owned());
Ok(()) Ok(())
} }
Some(Entry::Dir { .. }) => must_be_file(path), Some(Entry::Dir { .. }) => must_be_file(path),
@@ -131,9 +184,11 @@ impl VfsBackend for MemoryBackend {
} }
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> { fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
match self.entries.get(path) { let mut inner = self.inner.lock().unwrap();
match inner.entries.get(path) {
Some(Entry::Dir { .. }) => { Some(Entry::Dir { .. }) => {
self.remove(path.to_owned()); inner.remove(path.to_owned());
Ok(()) Ok(())
} }
Some(Entry::File { .. }) => must_be_dir(path), Some(Entry::File { .. }) => must_be_dir(path),
@@ -142,7 +197,9 @@ impl VfsBackend for MemoryBackend {
} }
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> { fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
match self.entries.get(path) { let inner = self.inner.lock().unwrap();
match inner.entries.get(path) {
Some(Entry::File { .. }) => Ok(Metadata { is_file: true }), Some(Entry::File { .. }) => Ok(Metadata { is_file: true }),
Some(Entry::Dir { .. }) => Ok(Metadata { is_file: false }), Some(Entry::Dir { .. }) => Ok(Metadata { is_file: false }),
None => not_found(path), None => not_found(path),
@@ -150,7 +207,9 @@ impl VfsBackend for MemoryBackend {
} }
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
crossbeam_channel::never() let inner = self.inner.lock().unwrap();
inner.event_receiver.clone()
} }
fn watch(&mut self, _path: &Path) -> io::Result<()> { fn watch(&mut self, _path: &Path) -> io::Result<()> {

View File

@@ -1,4 +1,23 @@
mod memory_backend; /*!
Implementation of a virtual filesystem with a configurable backend and file
watching.
memofs is currently an unstable minimum viable library. Its primary consumer is
[Rojo](https://github.com/rojo-rbx/rojo), a build system for Roblox.
## Current Features
* API similar to `std::fs`
* Configurable backends
* `StdBackend`, which uses `std::fs` and the `notify` crate
* `NoopBackend`, which always throws errors
* `InMemoryFs`, a simple in-memory filesystem useful for testing
## Future Features
* Hash-based hierarchical memoization keys (hence the name)
* Configurable caching (write-through, write-around, write-back)
*/
mod in_memory_fs;
mod noop_backend; mod noop_backend;
mod snapshot; mod snapshot;
mod std_backend; mod std_backend;
@@ -7,7 +26,7 @@ use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
pub use memory_backend::MemoryBackend; pub use in_memory_fs::InMemoryFs;
pub use noop_backend::NoopBackend; pub use noop_backend::NoopBackend;
pub use snapshot::VfsSnapshot; pub use snapshot::VfsSnapshot;
pub use std_backend::StdBackend; pub use std_backend::StdBackend;
@@ -18,9 +37,9 @@ mod sealed {
/// Sealing trait for VfsBackend. /// Sealing trait for VfsBackend.
pub trait Sealed {} pub trait Sealed {}
impl Sealed for MemoryBackend {}
impl Sealed for NoopBackend {} impl Sealed for NoopBackend {}
impl Sealed for StdBackend {} impl Sealed for StdBackend {}
impl Sealed for InMemoryFs {}
} }
/// Trait that transforms `io::Result<T>` into `io::Result<Option<T>>`. /// Trait that transforms `io::Result<T>` into `io::Result<Option<T>>`.
@@ -107,6 +126,8 @@ impl Metadata {
} }
} }
/// Represents an event that a filesystem can raise that might need to be
/// handled.
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
pub enum VfsEvent { pub enum VfsEvent {
@@ -176,6 +197,10 @@ impl VfsInner {
} }
/// A virtual filesystem with a configurable backend. /// A virtual filesystem with a configurable backend.
///
/// All operations on the Vfs take a lock on an internal backend. For performing
/// large batches of operations, it might be more performant to call `lock()`
/// and use [`VfsLock`](struct.VfsLock.html) instead.
pub struct Vfs { pub struct Vfs {
inner: Mutex<VfsInner>, inner: Mutex<VfsInner>,
} }
@@ -197,6 +222,7 @@ impl Vfs {
} }
} }
/// Manually lock the Vfs, useful for large batches of operations.
pub fn lock(&self) -> VfsLock<'_> { pub fn lock(&self) -> VfsLock<'_> {
VfsLock { VfsLock {
inner: self.inner.lock().unwrap(), inner: self.inner.lock().unwrap(),
@@ -284,7 +310,9 @@ impl Vfs {
} }
} }
/// A locked handle to a `Vfs`, created by `Vfs::lock`. /// A locked handle to a [`Vfs`](struct.Vfs.html), created by `Vfs::lock`.
///
/// Implements roughly the same API as [`Vfs`](struct.Vfs.html).
pub struct VfsLock<'a> { pub struct VfsLock<'a> {
inner: MutexGuard<'a, VfsInner>, inner: MutexGuard<'a, VfsInner>,
} }

View File

@@ -1,5 +1,9 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// A slice of a tree of files. Can be loaded into an
/// [`InMemoryFs`](struct.InMemoryFs.html).
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive] #[non_exhaustive]
pub enum VfsSnapshot { pub enum VfsSnapshot {
File { File {
@@ -26,4 +30,16 @@ impl VfsSnapshot {
.collect(), .collect(),
} }
} }
pub fn empty_file() -> Self {
Self::File {
contents: Vec::new(),
}
}
pub fn empty_dir() -> Self {
Self::Dir {
children: BTreeMap::new(),
}
}
} }

View File

@@ -1,4 +1,3 @@
use std::fs;
use std::io; use std::io;
use std::path::Path; use std::path::Path;
use std::sync::mpsc; use std::sync::mpsc;
@@ -55,19 +54,22 @@ impl StdBackend {
impl VfsBackend for StdBackend { impl VfsBackend for StdBackend {
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> { fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
fs::read(path) fs_err::read(path)
} }
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> { fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
fs::write(path, data) fs_err::write(path, data)
} }
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
let inner = fs::read_dir(path)?.map(|entry| { let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
Ok(DirEntry { let mut entries = entries?;
path: entry?.path(),
}) entries.sort_by_cached_key(|entry| entry.file_name());
});
let inner = entries
.into_iter()
.map(|entry| Ok(DirEntry { path: entry.path() }));
Ok(ReadDir { Ok(ReadDir {
inner: Box::new(inner), inner: Box::new(inner),
@@ -75,15 +77,15 @@ impl VfsBackend for StdBackend {
} }
fn remove_file(&mut self, path: &Path) -> io::Result<()> { fn remove_file(&mut self, path: &Path) -> io::Result<()> {
fs::remove_file(path) fs_err::remove_file(path)
} }
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> { fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
fs::remove_dir_all(path) fs_err::remove_dir_all(path)
} }
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> { fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
let inner = fs::metadata(path)?; let inner = fs_err::metadata(path)?;
Ok(Metadata { Ok(Metadata {
is_file: inner.is_file(), is_file: inner.is_file(),

View File

@@ -14,6 +14,9 @@
"Fmt": { "Fmt": {
"$path": "fmt" "$path": "fmt"
}, },
"RbxDom": {
"$path": "rbx_dom_lua"
},
"Roact": { "Roact": {
"$path": "modules/roact/src" "$path": "modules/roact/src"
}, },
@@ -22,9 +25,6 @@
}, },
"t": { "t": {
"$path": "modules/t/lib" "$path": "modules/t/lib"
},
"RbxDom": {
"$path": "modules/rbx-dom/rbx_dom_lua/src"
} }
} }
} }

View File

@@ -0,0 +1,44 @@
stds.roblox = {
read_globals = {
game = {
other_fields = true,
},
-- Roblox globals
"script",
-- Extra functions
"tick", "warn",
"wait", "typeof",
-- Types
"CFrame",
"Color3",
"Enum",
"Instance",
"NumberRange",
"Rect",
"UDim", "UDim2",
"Vector2", "Vector3",
"Vector2int16", "Vector3int16",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
}
std = "lua51+roblox"
files["**/*.spec.lua"] = {
std = "+testez",
}

View File

@@ -0,0 +1,2 @@
# rbx\_dom\_lua
Roblox Lua implementation of rbx-dom mechanisms, intended to work with rbx\_dom\_weak and friends.

View File

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

View File

@@ -0,0 +1,242 @@
local base64 = require(script.Parent.base64)
local function identity(...)
return ...
end
local function unpackDecoder(f)
return function(value)
return f(unpack(value))
end
end
local function serializeFloat(value)
-- TODO: Figure out a better way to serialize infinity and NaN, neither of
-- which fit into JSON.
if value == math.huge or value == -math.huge then
return 999999999 * math.sign(value)
end
return value
end
local encoders
encoders = {
Bool = identity,
Content = identity,
Float32 = serializeFloat,
Float64 = serializeFloat,
Int32 = identity,
Int64 = identity,
String = identity,
BinaryString = base64.encode,
SharedString = base64.encode,
BrickColor = function(value)
return value.Number
end,
CFrame = function(value)
return {value:GetComponents()}
end,
Color3 = function(value)
return {value.r, value.g, value.b}
end,
NumberRange = function(value)
return {value.Min, value.Max}
end,
NumberSequence = function(value)
local keypoints = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = {
Time = keypoint.Time,
Value = keypoint.Value,
Envelope = keypoint.Envelope,
}
end
return {
Keypoints = keypoints,
}
end,
ColorSequence = function(value)
local keypoints = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = {
Time = keypoint.Time,
Color = encoders.Color3(keypoint.Value),
}
end
return {
Keypoints = keypoints,
}
end,
Rect = function(value)
return {
Min = {value.Min.X, value.Min.Y},
Max = {value.Max.X, value.Max.Y},
}
end,
UDim = function(value)
return {value.Scale, value.Offset}
end,
UDim2 = function(value)
return {value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset}
end,
Vector2 = function(value)
return {
serializeFloat(value.X),
serializeFloat(value.Y),
}
end,
Vector2int16 = function(value)
return {value.X, value.Y}
end,
Vector3 = function(value)
return {
serializeFloat(value.X),
serializeFloat(value.Y),
serializeFloat(value.Z),
}
end,
Vector3int16 = function(value)
return {value.X, value.Y, value.Z}
end,
PhysicalProperties = function(value)
if value == nil then
return nil
else
return {
Density = value.Density,
Friction = value.Friction,
Elasticity = value.Elasticity,
FrictionWeight = value.FrictionWeight,
ElasticityWeight = value.ElasticityWeight,
}
end
end,
Ref = function(value)
return nil
end,
}
local decoders = {
Bool = identity,
Content = identity,
Enum = identity,
Float32 = identity,
Float64 = identity,
Int32 = identity,
Int64 = identity,
String = identity,
BinaryString = base64.decode,
SharedString = base64.decode,
BrickColor = BrickColor.new,
CFrame = unpackDecoder(CFrame.new),
Color3 = unpackDecoder(Color3.new),
Color3uint8 = unpackDecoder(Color3.fromRGB),
NumberRange = unpackDecoder(NumberRange.new),
UDim = unpackDecoder(UDim.new),
UDim2 = unpackDecoder(UDim2.new),
Vector2 = unpackDecoder(Vector2.new),
Vector2int16 = unpackDecoder(Vector2int16.new),
Vector3 = unpackDecoder(Vector3.new),
Vector3int16 = unpackDecoder(Vector3int16.new),
Rect = function(value)
return Rect.new(value.Min[1], value.Min[2], value.Max[1], value.Max[2])
end,
NumberSequence = function(value)
local keypoints = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = NumberSequenceKeypoint.new(
keypoint.Time,
keypoint.Value,
keypoint.Envelope
)
end
return NumberSequence.new(keypoints)
end,
ColorSequence = function(value)
local keypoints = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = ColorSequenceKeypoint.new(
keypoint.Time,
Color3.new(unpack(keypoint.Color))
)
end
return ColorSequence.new(keypoints)
end,
PhysicalProperties = function(properties)
if properties == nil then
return nil
else
return PhysicalProperties.new(
properties.Density,
properties.Friction,
properties.Elasticity,
properties.FrictionWeight,
properties.ElasticityWeight
)
end
end,
Ref = function()
return nil
end,
}
local EncodedValue = {}
function EncodedValue.decode(encodedValue)
local decoder = decoders[encodedValue.Type]
if decoder ~= nil then
return true, decoder(encodedValue.Value)
end
return false, "Couldn't decode value " .. tostring(encodedValue.Type)
end
function EncodedValue.encode(rbxValue, propertyType)
assert(propertyType ~= nil, "Property type descriptor is required")
if propertyType.type == "Data" then
local encoder = encoders[propertyType.name]
if encoder == nil then
return false, ("Missing encoder for property type %q"):format(propertyType.name)
end
if encoder ~= nil then
return true, {
Type = propertyType.name,
Value = encoder(rbxValue),
}
end
elseif propertyType.type == "Enum" then
return true, {
Type = "Enum",
Value = rbxValue.Value,
}
end
return false, ("Unknown property descriptor type %q"):format(tostring(propertyType.type))
end
return EncodedValue

View File

@@ -0,0 +1,127 @@
return function()
local RbxDom = require(script.Parent)
local EncodedValue = require(script.Parent.EncodedValue)
it("should decode Rect values", function()
local input = {
Type = "Rect",
Value = {
Min = {1, 2},
Max = {3, 4},
},
}
local output = Rect.new(1, 2, 3, 4)
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
it("should decode ColorSequence values", function()
local input = {
Type = "ColorSequence",
Value = {
Keypoints = {
{
Time = 0,
Color = { 0.12, 0.34, 0.56 },
},
{
Time = 1,
Color = { 0.13, 0.33, 0.37 },
},
}
},
}
local output = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(0.12, 0.34, 0.56)),
ColorSequenceKeypoint.new(1, Color3.new(0.13, 0.33, 0.37)),
})
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
it("should decode NumberSequence values", function()
local input = {
Type = "NumberSequence",
Value = {
Keypoints = {
{
Time = 0,
Value = 0.5,
Envelope = 0,
},
{
Time = 1,
Value = 0.5,
Envelope = 0,
},
}
},
}
local output = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.5, 0),
NumberSequenceKeypoint.new(1, 0.5, 0),
})
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
it("should decode PhysicalProperties values", function()
local input = {
Type = "PhysicalProperties",
Value = {
Density = 0.1,
Friction = 0.2,
Elasticity = 0.3,
FrictionWeight = 0.4,
ElasticityWeight = 0.5,
},
}
local output = PhysicalProperties.new(
0.1,
0.2,
0.3,
0.4,
0.5
)
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
-- This part of rbx_dom_lua needs some work still.
itSKIP("should encode Rect values", function()
local input = Rect.new(10, 20, 30, 40)
local output = {
Type = "Rect",
Value = {
Min = {10, 20},
Max = {30, 40},
},
}
local descriptor = RbxDom.findCanonicalPropertyDescriptor("ImageLabel", "SliceCenter")
local ok, encoded = EncodedValue.encode(input, descriptor)
assert(ok, encoded)
expect(encoded.Type).to.equal(output.Type)
expect(encoded.Value.Min[1]).to.equal(output.Value.Min[1])
expect(encoded.Value.Min[2]).to.equal(output.Value.Min[2])
expect(encoded.Value.Max[1]).to.equal(output.Value.Max[1])
expect(encoded.Value.Max[2]).to.equal(output.Value.Max[2])
end)
end

View File

@@ -0,0 +1,28 @@
local Error = {}
Error.__index = Error
Error.Kind = {
UnknownProperty = "UnknownProperty",
PropertyNotReadable = "PropertyNotReadable",
PropertyNotWritable = "PropertyNotWritable",
Roblox = "Roblox",
}
setmetatable(Error.Kind, {
__index = function(_, key)
error(("%q is not a valid member of Error.Kind"):format(tostring(key)), 2)
end,
})
function Error.new(kind, extra)
return setmetatable({
kind = kind,
extra = extra,
}, Error)
end
function Error:__tostring()
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
end
return Error

View File

@@ -0,0 +1,80 @@
local Error = require(script.Parent.Error)
local customProperties = require(script.Parent.customProperties)
-- A wrapper around a property descriptor from the reflection database with some
-- extra convenience methods.
--
-- The aim of this API is to facilitate looking up a property once, then reading
-- from it or writing to it multiple times. It's also useful when a consumer
-- wants to check additional constraints on the property before trying to use
-- it, like scriptability.
local PropertyDescriptor = {}
PropertyDescriptor.__index = PropertyDescriptor
local function get(container, key)
return container[key]
end
local function set(container, key, value)
container[key] = value
end
function PropertyDescriptor.fromRaw(data, className, propertyName)
return setmetatable({
scriptability = data.scriptability,
className = className,
name = propertyName,
}, PropertyDescriptor)
end
function PropertyDescriptor:read(instance)
if self.scriptability == "ReadWrite" or self.scriptability == "Read" then
local success, value = xpcall(get, debug.traceback, instance, self.name)
if success then
return success, value
else
return false, Error.new(Error.Kind.Roblox, value)
end
end
if self.scriptability == "Custom" then
local interface = customProperties[self.className][self.name]
return interface.read(instance, self.name)
end
if self.scriptability == "None" or self.scriptability == "Write" then
local fullName = ("%s.%s"):format(instance.className, self.name)
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
end
error(("Internal error: unexpected value of 'scriptability': %s"):format(tostring(self.scriptability)), 2)
end
function PropertyDescriptor:write(instance, value)
if self.scriptability == "ReadWrite" or self.scriptability == "Write" then
local success, err = xpcall(set, debug.traceback, instance, self.name, value)
if success then
return success
else
return false, Error.new(Error.Kind.Roblox, err)
end
end
if self.scriptability == "Custom" then
local interface = customProperties[self.className][self.name]
return interface.write(instance, self.name, value)
end
if self.scriptability == "None" or self.scriptability == "Read" then
local fullName = ("%s.%s"):format(instance.className, self.name)
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
end
end
return PropertyDescriptor

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
return {
classes = require(script.classes)
}

View File

@@ -0,0 +1,139 @@
-- Thanks to Tiffany352 for this base64 implementation!
local floor = math.floor
local char = string.char
local function encodeBase64(str)
local out = {}
local nOut = 0
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local strLen = #str
-- 3 octets become 4 hextets
for i = 1, strLen - 2, 3 do
local b1, b2, b3 = str:byte(i, i + 3)
local word = b3 + b2 * 256 + b1 * 256 * 256
local h4 = word % 64 + 1
word = floor(word / 64)
local h3 = word % 64 + 1
word = floor(word / 64)
local h2 = word % 64 + 1
word = floor(word / 64)
local h1 = word % 64 + 1
out[nOut + 1] = alphabet:sub(h1, h1)
out[nOut + 2] = alphabet:sub(h2, h2)
out[nOut + 3] = alphabet:sub(h3, h3)
out[nOut + 4] = alphabet:sub(h4, h4)
nOut = nOut + 4
end
local remainder = strLen % 3
if remainder == 2 then
-- 16 input bits -> 3 hextets (2 full, 1 partial)
local b1, b2 = str:byte(-2, -1)
-- partial is 4 bits long, leaving 2 bits of zero padding ->
-- offset = 4
local word = b2 * 4 + b1 * 4 * 256
local h3 = word % 64 + 1
word = floor(word / 64)
local h2 = word % 64 + 1
word = floor(word / 64)
local h1 = word % 64 + 1
out[nOut + 1] = alphabet:sub(h1, h1)
out[nOut + 2] = alphabet:sub(h2, h2)
out[nOut + 3] = alphabet:sub(h3, h3)
out[nOut + 4] = "="
elseif remainder == 1 then
-- 8 input bits -> 2 hextets (2 full, 1 partial)
local b1 = str:byte(-1, -1)
-- partial is 2 bits long, leaving 4 bits of zero padding ->
-- offset = 16
local word = b1 * 16
local h2 = word % 64 + 1
word = floor(word / 64)
local h1 = word % 64 + 1
out[nOut + 1] = alphabet:sub(h1, h1)
out[nOut + 2] = alphabet:sub(h2, h2)
out[nOut + 3] = "="
out[nOut + 4] = "="
end
-- if the remainder is 0, then no work is needed
return table.concat(out, "")
end
local function decodeBase64(str)
local out = {}
local nOut = 0
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local strLen = #str
local acc = 0
local nAcc = 0
local alphabetLut = {}
for i = 1, #alphabet do
alphabetLut[alphabet:sub(i, i)] = i - 1
end
-- 4 hextets become 3 octets
for i = 1, strLen do
local ch = str:sub(i, i)
local byte = alphabetLut[ch]
if byte then
acc = acc * 64 + byte
nAcc = nAcc + 1
end
if nAcc == 4 then
local b3 = acc % 256
acc = floor(acc / 256)
local b2 = acc % 256
acc = floor(acc / 256)
local b1 = acc % 256
out[nOut + 1] = char(b1)
out[nOut + 2] = char(b2)
out[nOut + 3] = char(b3)
nOut = nOut + 3
nAcc = 0
acc = 0
end
end
if nAcc == 3 then
-- 3 hextets -> 16 bit output
acc = acc * 64
acc = floor(acc / 256)
local b2 = acc % 256
acc = floor(acc / 256)
local b1 = acc % 256
out[nOut + 1] = char(b1)
out[nOut + 2] = char(b2)
elseif nAcc == 2 then
-- 2 hextets -> 8 bit output
acc = acc * 64
acc = floor(acc / 256)
acc = acc * 64
acc = floor(acc / 256)
local b1 = acc % 256
out[nOut + 1] = char(b1)
elseif nAcc == 1 then
error("Base64 has invalid length")
end
return table.concat(out, "")
end
return {
decode = decodeBase64,
encode = encodeBase64,
}

View File

@@ -0,0 +1,29 @@
return function()
local base64 = require(script.Parent.base64)
it("should encode and decode", function()
local function try(str, expected)
local encoded = base64.encode(str)
expect(encoded).to.equal(expected)
expect(base64.decode(encoded)).to.equal(str)
end
try("Man", "TWFu")
try("Ma", "TWE=")
try("M", "TQ==")
try("ManM", "TWFuTQ==")
try(
[[Man is distinguished, not only by his reason, but by this ]]..
[[singular passion from other animals, which is a lust of the ]]..
[[mind, that by a perseverance of delight in the continued and ]]..
[[indefatigable generation of knowledge, exceeds the short ]]..
[[vehemence of any carnal pleasure.]],
[[TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sI]]..
[[GJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYW]]..
[[xzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJ]]..
[[zZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRl]]..
[[ZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZ]]..
[[SBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=]]
)
end)
end

View File

@@ -0,0 +1,47 @@
local CollectionService = game:GetService("CollectionService")
-- Defines how to read and write properties that aren't directly scriptable.
--
-- The reflection database refers to these as having scriptability = "Custom"
return {
Instance = {
Tags = {
read = function(instance, key)
local tagList = CollectionService:GetTags(instance)
return true, table.concat(tagList, "\0")
end,
write = function(instance, key, value)
local existingTags = CollectionService:GetTags(instance)
local unseenTags = {}
for _, tag in ipairs(existingTags) do
unseenTags[tag] = true
end
local tagList = string.split(value, "\0")
for _, tag in ipairs(tagList) do
unseenTags[tag] = nil
CollectionService:AddTag(instance, tag)
end
for tag in pairs(unseenTags) do
CollectionService:RemoveTag(instance, tag)
end
return true
end,
},
},
LocalizationTable = {
Contents = {
read = function(instance, key)
return true, instance:GetContents()
end,
write = function(instance, key, value)
instance:SetContents(value)
return true
end,
},
},
}

View File

@@ -0,0 +1,67 @@
local ReflectionDatabase = require(script.ReflectionDatabase)
local Error = require(script.Error)
local PropertyDescriptor = require(script.PropertyDescriptor)
local function findCanonicalPropertyDescriptor(className, propertyName)
local currentClassName = className
repeat
local currentClass = ReflectionDatabase.classes[currentClassName]
if currentClass == nil then
return currentClass
end
local propertyData = currentClass.properties[propertyName]
if propertyData ~= nil then
if propertyData.isCanonical then
return PropertyDescriptor.fromRaw(propertyData, currentClassName, propertyName)
end
if propertyData.canonicalName ~= nil then
return PropertyDescriptor.fromRaw(
currentClass.properties[propertyData.canonicalName],
currentClassName,
propertyData.canonicalName)
end
return nil
end
currentClassName = currentClass.superclass
until currentClassName == nil
return nil
end
local function readProperty(instance, propertyName)
local descriptor = findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
if descriptor == nil then
local fullName = ("%s.%s"):format(instance.className, propertyName)
return false, Error.new(Error.Kind.UnknownProperty, fullName)
end
return descriptor:read(instance)
end
local function writeProperty(instance, propertyName, value)
local descriptor = findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
if descriptor == nil then
local fullName = ("%s.%s"):format(instance.className, propertyName)
return false, Error.new(Error.Kind.UnknownProperty, fullName)
end
return descriptor:write(instance, value)
end
return {
readProperty = readProperty,
writeProperty = writeProperty,
findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor,
Error = Error,
EncodedValue = require(script.EncodedValue),
}

View File

@@ -0,0 +1,7 @@
return function()
local RbxDom = require(script.Parent)
it("should load", function()
expect(RbxDom).to.be.ok()
end)
end

View File

@@ -0,0 +1,35 @@
{
"name": "rbx_dom_lua test place",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"RbxDom": {
"$path": "src"
},
"TestEZ": {
"$path": "modules/testez/lib"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"Run Tests": {
"$path": "test.server.lua"
}
},
"Players": {
"$className": "Players",
"$properties": {
"CharacterAutoLoads": false
}
},
"HttpService": {
"$className": "HttpService",
"$properties": {
"HttpEnabled": true
}
}
}
}

View File

@@ -0,0 +1,7 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LIB_ROOT = ReplicatedStorage.RbxDom
local TestEZ = require(ReplicatedStorage.TestEZ)
TestEZ.TestBootstrap:run({LIB_ROOT})

View File

@@ -233,4 +233,19 @@ function ApiContext:retrieveMessages()
end) end)
end end
function ApiContext:open(id)
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
return Http.post(url, "")
:andThen(rejectFailedRequests)
:andThen(Http.Response.json)
:andThen(function(body)
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
return nil
end)
end
return ApiContext return ApiContext

View File

@@ -17,6 +17,7 @@ local ConnectPanel = require(Plugin.Components.ConnectPanel)
local ConnectingPanel = require(Plugin.Components.ConnectingPanel) local ConnectingPanel = require(Plugin.Components.ConnectingPanel)
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel) local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
local ErrorPanel = require(Plugin.Components.ErrorPanel) local ErrorPanel = require(Plugin.Components.ErrorPanel)
local SettingsPanel = require(Plugin.Components.SettingsPanel)
local e = Roact.createElement local e = Roact.createElement
@@ -61,6 +62,7 @@ local AppStatus = strict("AppStatus", {
Connecting = "Connecting", Connecting = "Connecting",
Connected = "Connected", Connected = "Connected",
Error = "Error", Error = "Error",
Settings = "Settings",
}) })
local App = Roact.Component:extend("App") local App = Roact.Component:extend("App")
@@ -73,10 +75,7 @@ function App:init()
self.signals = {} self.signals = {}
self.serveSession = nil self.serveSession = nil
self.displayedVersion = Version.display(Config.version)
self.displayedVersion = DevSettings:isEnabled()
and Config.codename
or Version.display(Config.version)
local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion) local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion)
@@ -108,12 +107,14 @@ function App:init()
end) end)
end end
function App:startSession(address, port) function App:startSession(address, port, sessionOptions)
Log.trace("Starting new session") Log.trace("Starting new session")
local baseUrl = ("http://%s:%s"):format(address, port) local baseUrl = ("http://%s:%s"):format(address, port)
self.serveSession = ServeSession.new({ self.serveSession = ServeSession.new({
apiContext = ApiContext.new(baseUrl), apiContext = ApiContext.new(baseUrl),
openScriptsExternally = sessionOptions.openScriptsExternally,
twoWaySync = sessionOptions.twoWaySync,
}) })
self.serveSession:onStatusChanged(function(status, details) self.serveSession:onStatusChanged(function(status, details)
@@ -154,8 +155,13 @@ function App:render()
if self.state.appStatus == AppStatus.NotStarted then if self.state.appStatus == AppStatus.NotStarted then
children = { children = {
ConnectPanel = e(ConnectPanel, { ConnectPanel = e(ConnectPanel, {
startSession = function(address, port) startSession = function(address, port, settings)
self:startSession(address, port) self:startSession(address, port, settings)
end,
openSettings = function()
self:setState({
appStatus = AppStatus.Settings,
})
end, end,
cancel = function() cancel = function()
Log.trace("Canceling session configuration") Log.trace("Canceling session configuration")
@@ -168,7 +174,7 @@ function App:render()
} }
elseif self.state.appStatus == AppStatus.Connecting then elseif self.state.appStatus == AppStatus.Connecting then
children = { children = {
ConnectingPanel = Roact.createElement(ConnectingPanel), ConnectingPanel = e(ConnectingPanel),
} }
elseif self.state.appStatus == AppStatus.Connected then elseif self.state.appStatus == AppStatus.Connected then
children = { children = {
@@ -186,9 +192,19 @@ function App:render()
end, end,
}), }),
} }
elseif self.state.appStatus == AppStatus.Settings then
children = {
e(SettingsPanel, {
back = function()
self:setState({
appStatus = AppStatus.NotStarted,
})
end,
}),
}
elseif self.state.appStatus == AppStatus.Error then elseif self.state.appStatus == AppStatus.Error then
children = { children = {
ErrorPanel = Roact.createElement(ErrorPanel, { ErrorPanel = e(ErrorPanel, {
errorMessage = self.state.errorMessage, errorMessage = self.state.errorMessage,
onDismiss = function() onDismiss = function()
self:setState({ self:setState({
@@ -199,7 +215,7 @@ function App:render()
} }
end end
return Roact.createElement(Roact.Portal, { return e(Roact.Portal, {
target = self.dockWidget, target = self.dockWidget,
}, children) }, children)
end end

View File

@@ -0,0 +1,39 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement
local function Checkbox(props)
local checked = props.checked
local layoutOrder = props.layoutOrder
local onChange = props.onChange
return Theme.with(function(theme)
return e("ImageButton", {
LayoutOrder = layoutOrder,
Size = UDim2.new(0, 20, 0, 20),
BorderSizePixel = 2,
BorderColor3 = theme.Text2,
BackgroundColor3 = theme.Background2,
[Roact.Event.Activated] = function()
onChange(not checked)
end,
}, {
Indicator = e("Frame", {
Size = UDim2.new(0, 18, 0, 18),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BorderSizePixel = 0,
BackgroundColor3 = theme.Brand1,
BackgroundTransparency = checked and 0 or 1,
})
})
end)
end
return Checkbox

View File

@@ -4,13 +4,14 @@ local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact) local Roact = require(Rojo.Roact)
local Config = require(Plugin.Config) local Config = require(Plugin.Config)
local Theme = require(Plugin.Theme)
local Theme = require(Plugin.Components.Theme)
local Panel = require(Plugin.Components.Panel) local Panel = require(Plugin.Components.Panel)
local FitList = require(Plugin.Components.FitList) local FitList = require(Plugin.Components.FitList)
local FitText = require(Plugin.Components.FitText) local FitText = require(Plugin.Components.FitText)
local FormButton = require(Plugin.Components.FormButton) local FormButton = require(Plugin.Components.FormButton)
local FormTextInput = require(Plugin.Components.FormTextInput) local FormTextInput = require(Plugin.Components.FormTextInput)
local PluginSettings = require(Plugin.Components.PluginSettings)
local e = Roact.createElement local e = Roact.createElement
@@ -25,137 +26,158 @@ end
function ConnectPanel:render() function ConnectPanel:render()
local startSession = self.props.startSession local startSession = self.props.startSession
local openSettings = self.props.openSettings
return e(Panel, nil, { return Theme.with(function(theme)
Layout = e("UIListLayout", { return PluginSettings.with(function(settings)
SortOrder = Enum.SortOrder.LayoutOrder, return e(Panel, nil, {
HorizontalAlignment = Enum.HorizontalAlignment.Center, Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder,
}), HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Inputs = e(FitList, {
containerProps = {
BackgroundTransparency = 1,
LayoutOrder = 1,
},
layoutProps = {
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 8),
},
paddingProps = {
PaddingTop = UDim.new(0, 20),
PaddingBottom = UDim.new(0, 10),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
Address = e(FitList, {
containerProps = {
LayoutOrder = 1,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = Theme.TitleFont,
TextSize = 20,
Text = "Address",
TextColor3 = Theme.PrimaryColor,
}), }),
Input = e(FormTextInput, { Inputs = e(FitList, {
layoutOrder = 2, containerProps = {
width = UDim.new(0, 220), BackgroundTransparency = 1,
value = self.state.address, LayoutOrder = 1,
placeholderValue = Config.defaultHost, },
onValueChange = function(newValue) layoutProps = {
self:setState({ FillDirection = Enum.FillDirection.Horizontal,
address = newValue, Padding = UDim.new(0, 8),
}) },
end, paddingProps = {
}), PaddingTop = UDim.new(0, 20),
}), PaddingBottom = UDim.new(0, 10),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
Address = e(FitList, {
containerProps = {
LayoutOrder = 1,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.TitleFont,
TextSize = 20,
Text = "Address",
TextColor3 = theme.Text1,
}),
Port = e(FitList, { Input = e(FormTextInput, {
containerProps = { layoutOrder = 2,
LayoutOrder = 2, width = UDim.new(0, 220),
BackgroundTransparency = 1, value = self.state.address,
}, placeholderValue = Config.defaultHost,
layoutProps = { onValueChange = function(newValue)
Padding = UDim.new(0, 4), self:setState({
}, address = newValue,
}, { })
Label = e(FitText, { end,
Kind = "TextLabel", }),
LayoutOrder = 1, }),
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left, Port = e(FitList, {
Font = Theme.TitleFont, containerProps = {
TextSize = 20, LayoutOrder = 2,
Text = "Port", BackgroundTransparency = 1,
TextColor3 = Theme.PrimaryColor, },
layoutProps = {
Padding = UDim.new(0, 4),
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.TitleFont,
TextSize = 20,
Text = "Port",
TextColor3 = theme.Text1,
}),
Input = e(FormTextInput, {
layoutOrder = 2,
width = UDim.new(0, 80),
value = self.state.port,
placeholderValue = Config.defaultPort,
onValueChange = function(newValue)
self:setState({
port = newValue,
})
end,
}),
}),
}), }),
Input = e(FormTextInput, { Buttons = e(FitList, {
layoutOrder = 2, fitAxes = "Y",
width = UDim.new(0, 80), containerProps = {
value = self.state.port, BackgroundTransparency = 1,
placeholderValue = Config.defaultPort, LayoutOrder = 2,
onValueChange = function(newValue) Size = UDim2.new(1, 0, 0, 0),
self:setState({ },
port = newValue, layoutProps = {
}) FillDirection = Enum.FillDirection.Horizontal,
end, HorizontalAlignment = Enum.HorizontalAlignment.Right,
Padding = UDim.new(0, 8),
},
paddingProps = {
PaddingTop = UDim.new(0, 0),
PaddingBottom = UDim.new(0, 20),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
e(FormButton, {
layoutOrder = 1,
text = "Settings",
secondary = true,
onClick = function()
if openSettings ~= nil then
openSettings()
end
end,
}),
e(FormButton, {
layoutOrder = 2,
text = "Connect",
onClick = function()
if startSession ~= nil then
local address = self.state.address
if address:len() == 0 then
address = Config.defaultHost
end
local port = self.state.port
if port:len() == 0 then
port = Config.defaultPort
end
local sessionOptions = {
openScriptsExternally = settings:get("openScriptsExternally"),
twoWaySync = settings:get("twoWaySync"),
}
startSession(address, port, sessionOptions)
end
end,
}),
}), }),
}), })
}), end)
end)
Buttons = e(FitList, {
fitAxes = "Y",
containerProps = {
BackgroundTransparency = 1,
LayoutOrder = 2,
Size = UDim2.new(1, 0, 0, 0),
},
layoutProps = {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
Padding = UDim.new(0, 8),
},
paddingProps = {
PaddingTop = UDim.new(0, 0),
PaddingBottom = UDim.new(0, 20),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
e(FormButton, {
layoutOrder = 2,
text = "Connect",
onClick = function()
if startSession ~= nil then
local address = self.state.address
if address:len() == 0 then
address = Config.defaultHost
end
local port = self.state.port
if port:len() == 0 then
port = Config.defaultPort
end
startSession(address, port)
end
end,
}),
}),
})
end end
return ConnectPanel return ConnectPanel

View File

@@ -2,8 +2,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin") local Plugin = script:FindFirstAncestor("Plugin")
local Theme = require(Plugin.Theme) local Theme = require(Plugin.Components.Theme)
local Panel = require(Plugin.Components.Panel) local Panel = require(Plugin.Components.Panel)
local FitText = require(Plugin.Components.FitText) local FitText = require(Plugin.Components.FitText)
@@ -12,23 +11,25 @@ local e = Roact.createElement
local ConnectingPanel = Roact.Component:extend("ConnectingPanel") local ConnectingPanel = Roact.Component:extend("ConnectingPanel")
function ConnectingPanel:render() function ConnectingPanel:render()
return e(Panel, nil, { return Theme.with(function(theme)
Layout = Roact.createElement("UIListLayout", { return e(Panel, nil, {
HorizontalAlignment = Enum.HorizontalAlignment.Center, Layout = Roact.createElement("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center, HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8), SortOrder = Enum.SortOrder.LayoutOrder,
}), Padding = UDim.new(0, 8),
}),
Text = e(FitText, { Text = e(FitText, {
Padding = Vector2.new(12, 6), Padding = Vector2.new(12, 6),
Font = Theme.ButtonFont, Font = theme.ButtonFont,
TextSize = 18, TextSize = 18,
Text = "Connecting...", Text = "Connecting...",
TextColor3 = Theme.PrimaryColor, TextColor3 = theme.Text1,
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
}) })
end)
end end
return ConnectingPanel return ConnectingPanel

View File

@@ -2,8 +2,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin") local Plugin = script:FindFirstAncestor("Plugin")
local Theme = require(Plugin.Theme) local Theme = require(Plugin.Components.Theme)
local Panel = require(Plugin.Components.Panel) local Panel = require(Plugin.Components.Panel)
local FitText = require(Plugin.Components.FitText) local FitText = require(Plugin.Components.FitText)
local FormButton = require(Plugin.Components.FormButton) local FormButton = require(Plugin.Components.FormButton)
@@ -15,32 +14,34 @@ local ConnectionActivePanel = Roact.Component:extend("ConnectionActivePanel")
function ConnectionActivePanel:render() function ConnectionActivePanel:render()
local stopSession = self.props.stopSession local stopSession = self.props.stopSession
return e(Panel, nil, { return Theme.with(function(theme)
Layout = Roact.createElement("UIListLayout", { return e(Panel, nil, {
HorizontalAlignment = Enum.HorizontalAlignment.Center, Layout = Roact.createElement("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center, HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8), SortOrder = Enum.SortOrder.LayoutOrder,
}), Padding = UDim.new(0, 8),
}),
Text = e(FitText, { Text = e(FitText, {
Padding = Vector2.new(12, 6), Padding = Vector2.new(12, 6),
Font = Theme.ButtonFont, Font = theme.ButtonFont,
TextSize = 18, TextSize = 18,
Text = "Connected to Live-Sync Server", Text = "Connected to Live-Sync Server",
TextColor3 = Theme.PrimaryColor, TextColor3 = theme.Text1,
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
DisconnectButton = e(FormButton, { DisconnectButton = e(FormButton, {
layoutOrder = 2, layoutOrder = 2,
text = "Disconnect", text = "Disconnect",
secondary = true, secondary = true,
onClick = function() onClick = function()
stopSession() stopSession()
end, end,
}), }),
}) })
end)
end end
return ConnectionActivePanel return ConnectionActivePanel

View File

@@ -2,8 +2,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin") local Plugin = script:FindFirstAncestor("Plugin")
local Theme = require(Plugin.Theme) local Theme = require(Plugin.Components.Theme)
local Panel = require(Plugin.Components.Panel) local Panel = require(Plugin.Components.Panel)
local FitText = require(Plugin.Components.FitText) local FitText = require(Plugin.Components.FitText)
local FitScrollingFrame = require(Plugin.Components.FitScrollingFrame) local FitScrollingFrame = require(Plugin.Components.FitScrollingFrame)
@@ -20,50 +19,52 @@ function ErrorPanel:render()
local errorMessage = self.props.errorMessage local errorMessage = self.props.errorMessage
local onDismiss = self.props.onDismiss local onDismiss = self.props.onDismiss
return e(Panel, nil, { return Theme.with(function(theme)
Layout = Roact.createElement("UIListLayout", { return e(Panel, nil, {
HorizontalAlignment = Enum.HorizontalAlignment.Center, Layout = Roact.createElement("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center, HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 8), SortOrder = Enum.SortOrder.LayoutOrder,
}), Padding = UDim.new(0, 8),
ErrorContainer = e(FitScrollingFrame, {
containerProps = {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(1, -HOR_PADDING * 2, 1, -BUTTON_HEIGHT),
Position = UDim2.new(0, HOR_PADDING, 0, 0),
ScrollBarImageColor3 = Theme.PrimaryColor,
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
ScrollingDirection = Enum.ScrollingDirection.Y,
},
}, {
Text = e(FitText, {
Size = UDim2.new(1, 0, 0, 0),
LayoutOrder = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
FitAxis = "Y",
Font = Theme.ButtonFont,
TextSize = 18,
Text = errorMessage,
TextWrap = true,
TextColor3 = Theme.PrimaryColor,
BackgroundTransparency = 1,
}), }),
}),
DismissButton = e(FormButton, { ErrorContainer = e(FitScrollingFrame, {
layoutOrder = 2, containerProps = {
text = "Dismiss", BackgroundTransparency = 1,
secondary = true, BorderSizePixel = 0,
onClick = function() Size = UDim2.new(1, -HOR_PADDING * 2, 1, -BUTTON_HEIGHT),
onDismiss() Position = UDim2.new(0, HOR_PADDING, 0, 0),
end, ScrollBarImageColor3 = theme.Text1,
}), VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
}) ScrollingDirection = Enum.ScrollingDirection.Y,
},
}, {
Text = e(FitText, {
Size = UDim2.new(1, 0, 0, 0),
LayoutOrder = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
FitAxis = "Y",
Font = theme.ButtonFont,
TextSize = 18,
Text = errorMessage,
TextWrap = true,
TextColor3 = theme.Text1,
BackgroundTransparency = 1,
}),
}),
DismissButton = e(FormButton, {
layoutOrder = 2,
text = "Dismiss",
secondary = true,
onClick = function()
onDismiss()
end,
}),
})
end)
end end
return ErrorPanel return ErrorPanel

View File

@@ -4,7 +4,7 @@ local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact) local Roact = require(Rojo.Roact)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.Theme) local Theme = require(Plugin.Components.Theme)
local FitList = require(Plugin.Components.FitList) local FitList = require(Plugin.Components.FitList)
local FitText = require(Plugin.Components.FitText) local FitText = require(Plugin.Components.FitText)
@@ -20,43 +20,45 @@ local function FormButton(props)
local textColor local textColor
local backgroundColor local backgroundColor
if props.secondary then return Theme.with(function(theme)
textColor = Theme.AccentColor if props.secondary then
backgroundColor = Theme.SecondaryColor textColor = theme.Brand1
else backgroundColor = theme.Background2
textColor = Theme.SecondaryColor else
backgroundColor = Theme.AccentColor textColor = theme.TextOnAccent
end backgroundColor = theme.Brand1
end
return e(FitList, { return e(FitList, {
containerKind = "ImageButton", containerKind = "ImageButton",
containerProps = { containerProps = {
LayoutOrder = layoutOrder, LayoutOrder = layoutOrder,
BackgroundTransparency = 1, BackgroundTransparency = 1,
Image = RoundBox.asset, Image = RoundBox.asset,
ImageRectOffset = RoundBox.offset, ImageRectOffset = RoundBox.offset,
ImageRectSize = RoundBox.size, ImageRectSize = RoundBox.size,
SliceCenter = RoundBox.center, SliceCenter = RoundBox.center,
ScaleType = Enum.ScaleType.Slice, ScaleType = Enum.ScaleType.Slice,
ImageColor3 = backgroundColor, ImageColor3 = backgroundColor,
[Roact.Event.Activated] = function() [Roact.Event.Activated] = function()
if onClick ~= nil then if onClick ~= nil then
onClick() onClick()
end end
end, end,
}, },
}, { }, {
Text = e(FitText, { Text = e(FitText, {
Kind = "TextLabel", Kind = "TextLabel",
Text = text, Text = text,
TextSize = 18, TextSize = 18,
TextColor3 = textColor, TextColor3 = textColor,
Font = Theme.ButtonFont, Font = theme.ButtonFont,
Padding = Vector2.new(16, 8), Padding = Vector2.new(16, 8),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
}) })
end)
end end
return FormButton return FormButton

View File

@@ -4,7 +4,7 @@ local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact) local Roact = require(Rojo.Roact)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.Theme) local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement local e = Roact.createElement
@@ -35,46 +35,48 @@ function FormTextInput:render()
shownPlaceholder = placeholderValue shownPlaceholder = placeholderValue
end end
return e("ImageLabel", { return Theme.with(function(theme)
LayoutOrder = layoutOrder, return e("ImageLabel", {
Image = RoundBox.asset, LayoutOrder = layoutOrder,
ImageRectOffset = RoundBox.offset, Image = RoundBox.asset,
ImageRectSize = RoundBox.size, ImageRectOffset = RoundBox.offset,
ScaleType = Enum.ScaleType.Slice, ImageRectSize = RoundBox.size,
SliceCenter = RoundBox.center, ScaleType = Enum.ScaleType.Slice,
ImageColor3 = Theme.SecondaryColor, SliceCenter = RoundBox.center,
Size = UDim2.new(width.Scale, width.Offset, 0, TEXT_SIZE + PADDING * 2), ImageColor3 = theme.Background2,
BackgroundTransparency = 1, Size = UDim2.new(width.Scale, width.Offset, 0, TEXT_SIZE + PADDING * 2),
}, {
InputInner = e("TextBox", {
BackgroundTransparency = 1, BackgroundTransparency = 1,
Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2), }, {
Position = UDim2.new(0.5, 0, 0.5, 0), InputInner = e("TextBox", {
AnchorPoint = Vector2.new(0.5, 0.5), BackgroundTransparency = 1,
Font = Theme.InputFont, Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2),
ClearTextOnFocus = false, Position = UDim2.new(0.5, 0, 0.5, 0),
TextXAlignment = Enum.TextXAlignment.Center, AnchorPoint = Vector2.new(0.5, 0.5),
TextSize = TEXT_SIZE, Font = theme.InputFont,
Text = value, ClearTextOnFocus = false,
PlaceholderText = shownPlaceholder, TextXAlignment = Enum.TextXAlignment.Center,
PlaceholderColor3 = Theme.LightTextColor, TextSize = TEXT_SIZE,
TextColor3 = Theme.PrimaryColor, Text = value,
PlaceholderText = shownPlaceholder,
PlaceholderColor3 = theme.Text2,
TextColor3 = theme.Text1,
[Roact.Change.Text] = function(rbx) [Roact.Change.Text] = function(rbx)
onValueChange(rbx.Text) onValueChange(rbx.Text)
end, end,
[Roact.Event.Focused] = function() [Roact.Event.Focused] = function()
self:setState({ self:setState({
focused = true, focused = true,
}) })
end, end,
[Roact.Event.FocusLost] = function() [Roact.Event.FocusLost] = function()
self:setState({ self:setState({
focused = false, focused = false,
}) })
end, end,
}), }),
}) })
end)
end end
return FormTextInput return FormTextInput

View File

@@ -3,6 +3,7 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin") local Plugin = script:FindFirstAncestor("Plugin")
local RojoFooter = require(Plugin.Components.RojoFooter) local RojoFooter = require(Plugin.Components.RojoFooter)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement local e = Roact.createElement
@@ -13,22 +14,25 @@ function Panel:init()
end end
function Panel:render() function Panel:render()
return e("Frame", { return Theme.with(function(theme)
Size = UDim2.new(1, 0, 1, 0), return e("Frame", {
BackgroundTransparency = 1, Size = UDim2.new(1, 0, 1, 0),
}, { BackgroundColor3 = theme.Background1,
Layout = Roact.createElement("UIListLayout", { BorderSizePixel = 1,
HorizontalAlignment = Enum.HorizontalAlignment.Center, }, {
SortOrder = Enum.SortOrder.LayoutOrder, Layout = Roact.createElement("UIListLayout", {
}), HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Body = e("Frame", { Body = e("Frame", {
Size = UDim2.new(0, 360, 1, -32), Size = UDim2.new(0, 360, 1, -32),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, self.props[Roact.Children]), }, self.props[Roact.Children]),
Footer = e(RojoFooter), Footer = e(RojoFooter),
}) })
end)
end end
return Panel return Panel

View File

@@ -0,0 +1,121 @@
--[[
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,
}
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

@@ -6,7 +6,7 @@ local Roact = require(Rojo.Roact)
local Config = require(Plugin.Config) local Config = require(Plugin.Config)
local Version = require(Plugin.Version) local Version = require(Plugin.Version)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.Theme) local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement local e = Roact.createElement
@@ -18,50 +18,53 @@ function RojoFooter:init()
end end
function RojoFooter:render() function RojoFooter:render()
return e("Frame", { return Theme.with(function(theme)
LayoutOrder = 3, return e("Frame", {
Size = UDim2.new(1, 0, 0, 32), LayoutOrder = 3,
BackgroundColor3 = Theme.SecondaryColor, Size = UDim2.new(1, 0, 0, 32),
BorderSizePixel = 0, BackgroundColor3 = theme.Background2,
}, { BorderSizePixel = 0,
Padding = e("UIPadding", { ZIndex = 2,
PaddingTop = UDim.new(0, 4),
PaddingBottom = UDim.new(0, 4),
PaddingLeft = UDim.new(0, 8),
PaddingRight = UDim.new(0, 8),
}),
LogoContainer = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 0, 32),
}, { }, {
Logo = e("ImageLabel", { Padding = e("UIPadding", {
Image = Assets.Images.Logo, PaddingTop = UDim.new(0, 4),
Size = UDim2.new(0, 80, 0, 40), PaddingBottom = UDim.new(0, 4),
ScaleType = Enum.ScaleType.Fit, PaddingLeft = UDim.new(0, 8),
BackgroundTransparency = 1, PaddingRight = UDim.new(0, 8),
Position = UDim2.new(0, 0, 1, -10),
AnchorPoint = Vector2.new(0, 1),
}), }),
}),
Version = e("TextLabel", { LogoContainer = e("Frame", {
Position = UDim2.new(1, 0, 0, 0), BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(1, 0),
Font = Theme.TitleFont,
TextSize = 18,
Text = Version.display(Config.version),
TextXAlignment = Enum.TextXAlignment.Right,
TextColor3 = Theme.LightTextColor,
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = function(rbx) Size = UDim2.new(0, 0, 0, 32),
self.setFooterVersionSize(rbx.AbsoluteSize) }, {
end, Logo = e("ImageLabel", {
}), Image = Assets.Images.Logo,
}) Size = UDim2.new(0, 80, 0, 40),
ScaleType = Enum.ScaleType.Fit,
BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 1, -10),
AnchorPoint = Vector2.new(0, 1),
}),
}),
Version = e("TextLabel", {
Position = UDim2.new(1, 0, 0, 0),
Size = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(1, 0),
Font = theme.TitleFont,
TextSize = 18,
Text = Version.display(Config.version),
TextXAlignment = Enum.TextXAlignment.Right,
TextColor3 = theme.Text2,
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = function(rbx)
self.setFooterVersionSize(rbx.AbsoluteSize)
end,
}),
})
end)
end end
return RojoFooter return RojoFooter

View File

@@ -0,0 +1,119 @@
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin")
local Checkbox = require(Plugin.Components.Checkbox)
local FitList = require(Plugin.Components.FitList)
local FitText = require(Plugin.Components.FitText)
local FormButton = require(Plugin.Components.FormButton)
local Panel = require(Plugin.Components.Panel)
local PluginSettings = require(Plugin.Components.PluginSettings)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement
local SettingsPanel = Roact.Component:extend("SettingsPanel")
function SettingsPanel:render()
local back = self.props.back
return Theme.with(function(theme)
return PluginSettings.with(function(settings)
return e(Panel, nil, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 16),
}),
OpenScriptsExternally = e(FitList, {
containerProps = {
LayoutOrder = 1,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.MainFont,
TextSize = 16,
Text = "Open Scripts Externally",
TextColor3 = theme.Text1,
}),
Padding = e("Frame", {
Size = UDim2.new(0, 8, 0, 0),
BackgroundTransparency = 1,
LayoutOrder = 2,
}),
Input = e(Checkbox, {
layoutOrder = 3,
checked = settings:get("openScriptsExternally"),
onChange = function(newValue)
settings:set("openScriptsExternally", not settings:get("openScriptsExternally"))
end,
}),
}),
TwoWaySync = e(FitList, {
containerProps = {
LayoutOrder = 2,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.MainFont,
TextSize = 16,
Text = "Two-Way Sync (Experimental!)",
TextColor3 = theme.Text1,
}),
Padding = e("Frame", {
Size = UDim2.new(0, 8, 0, 0),
BackgroundTransparency = 1,
LayoutOrder = 2,
}),
Input = e(Checkbox, {
layoutOrder = 3,
checked = settings:get("twoWaySync"),
onChange = function(newValue)
settings:set("twoWaySync", not settings:get("twoWaySync"))
end,
}),
}),
BackButton = e(FormButton, {
layoutOrder = 4,
text = "Okay",
secondary = true,
onClick = function()
back()
end,
}),
})
end)
end)
end
return SettingsPanel

View File

@@ -0,0 +1,104 @@
--[[
Theming system taking advantage of Roact's new context API.
Doesn't use colors provided by Studio and instead just branches on theme
name. This isn't exactly best practice.
]]
local Studio = settings():GetService("Studio")
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local Log = require(Rojo.Log)
local strict = require(script.Parent.Parent.strict)
local lightTheme = strict("Theme", {
ButtonFont = Enum.Font.GothamSemibold,
InputFont = Enum.Font.Code,
TitleFont = Enum.Font.GothamBold,
MainFont = Enum.Font.Gotham,
Brand1 = Color3.fromRGB(225, 56, 53),
Text1 = Color3.fromRGB(64, 64, 64),
Text2 = Color3.fromRGB(160, 160, 160),
TextOnAccent = Color3.fromRGB(235, 235, 235),
Background1 = Color3.fromRGB(255, 255, 255),
Background2 = Color3.fromRGB(235, 235, 235),
})
local darkTheme = strict("Theme", {
ButtonFont = Enum.Font.GothamSemibold,
InputFont = Enum.Font.Code,
TitleFont = Enum.Font.GothamBold,
MainFont = Enum.Font.Gotham,
Brand1 = Color3.fromRGB(225, 56, 53),
Text1 = Color3.fromRGB(235, 235, 235),
Text2 = Color3.fromRGB(200, 200, 200),
TextOnAccent = Color3.fromRGB(235, 235, 235),
Background1 = Color3.fromRGB(48, 48, 48),
Background2 = Color3.fromRGB(64, 64, 64),
})
local Context = Roact.createContext(lightTheme)
local StudioProvider = Roact.Component:extend("StudioProvider")
-- Pull the current theme from Roblox Studio and update state with it.
function StudioProvider:updateTheme()
local studioTheme = Studio.Theme
if studioTheme.Name == "Light" then
self:setState({
theme = lightTheme,
})
elseif studioTheme.Name == "Dark" then
self:setState({
theme = darkTheme,
})
else
Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name)
self:setState({
theme = lightTheme,
})
end
end
function StudioProvider:init()
self:updateTheme()
end
function StudioProvider:render()
return Roact.createElement(Context.Provider, {
value = self.state.theme,
}, self.props[Roact.Children])
end
function StudioProvider:didMount()
self.connection = Studio.ThemeChanged:Connect(function()
self:updateTheme()
end)
end
function StudioProvider:willUnmount()
self.connection:Disconnect()
end
local function with(callback)
return Roact.createElement(Context.Consumer, {
render = callback,
})
end
return {
StudioProvider = StudioProvider,
Consumer = Context.Consumer,
with = with,
}

View File

@@ -5,8 +5,8 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
return strict("Config", { return strict("Config", {
isDevBuild = isDevBuild, isDevBuild = isDevBuild,
codename = "Epiphany", codename = "Epiphany",
version = {0, 6, 0, "-alpha.2"}, version = {6, 0, 0, "-rc.1"},
expectedServerVersionString = "0.6.0 or newer", expectedServerVersionString = "6.0 or newer",
protocolVersion = 3, protocolVersion = 3,
defaultHost = "localhost", defaultHost = "localhost",
defaultPort = 34872, defaultPort = 34872,

View File

@@ -25,14 +25,6 @@ local VALUES = {
[Environment.Test] = true, [Environment.Test] = true,
}, },
}, },
UnstableTwoWaySync = {
type = "BoolValue",
values = {
[Environment.User] = false,
[Environment.Dev] = false,
[Environment.Test] = false,
},
},
} }
local CONTAINER_NAME = "RojoDevSettings" .. Config.codename local CONTAINER_NAME = "RojoDevSettings" .. Config.codename
@@ -140,10 +132,6 @@ function DevSettings:shouldTypecheck()
return getValue("TypecheckingEnabled") return getValue("TypecheckingEnabled")
end end
function DevSettings:twoWaySyncEnabled()
return getValue("UnstableTwoWaySync")
end
function _G.ROJO_DEV_CREATE() function _G.ROJO_DEV_CREATE()
DevSettings:createDevSettings() DevSettings:createDevSettings()
end end

View File

@@ -11,9 +11,22 @@ InstanceMap.__index = InstanceMap
function InstanceMap.new(onInstanceChanged) function InstanceMap.new(onInstanceChanged)
local self = { local self = {
-- A map from IDs to instances.
fromIds = {}, fromIds = {},
-- A map from instances to IDs.
fromInstances = {}, fromInstances = {},
-- A set of all instances that updates should be paused for. This set
-- should generally be empty, and will be filled by pauseInstance
-- temporarily.
pausedUpdateInstances = {},
-- A map from instances to a signal or list of signals connected to it.
instancesToSignal = {}, instancesToSignal = {},
-- Callback that's invoked whenever an instance is changed and it was
-- not paused.
onInstanceChanged = onInstanceChanged, onInstanceChanged = onInstanceChanged,
} }
@@ -118,6 +131,32 @@ function InstanceMap:destroyId(id)
end end
end end
--[[
Pause updates for an instance momentarily and invoke a callback.
If the callback throws an error, InstanceMap will still be kept in a
consistent state.
]]
function InstanceMap:pauseInstance(instance, callback)
local id = self.fromInstances[instance]
-- If we don't know about this instance, ignore it and do not invoke the
-- callback.
if id == nil then
return
end
self.pausedUpdateInstances[instance] = true
local success, result = xpcall(callback, debug.traceback)
self.pausedUpdateInstances[instance] = false
if success then
return result
else
error(result, 2)
end
end
function InstanceMap:__connectSignals(instance) function InstanceMap:__connectSignals(instance)
-- ValueBase instances have an overriden version of the Changed signal that -- ValueBase instances have an overriden version of the Changed signal that
-- only detects changes to their Value property. -- only detects changes to their Value property.
@@ -150,9 +189,15 @@ end
function InstanceMap:__maybeFireInstanceChanged(instance, propertyName) function InstanceMap:__maybeFireInstanceChanged(instance, propertyName)
Log.trace("{}.{} changed", instance:GetFullName(), propertyName) Log.trace("{}.{} changed", instance:GetFullName(), propertyName)
if self.onInstanceChanged ~= nil then if self.pausedUpdateInstances[instance] then
self.onInstanceChanged(instance, propertyName) return
end end
if self.onInstanceChanged == nil then
return
end
self.onInstanceChanged(instance, propertyName)
end end
function InstanceMap:__disconnectSignals(instance) function InstanceMap:__disconnectSignals(instance)

25
plugin/src/PatchSet.lua Normal file
View File

@@ -0,0 +1,25 @@
--[[
Methods to operate on either a patch created by the hydrate method, or a
patch returned from the API.
]]
local t = require(script.Parent.Parent.t)
local Types = require(script.Parent.Types)
local PatchSet = {}
PatchSet.validate = t.interface({
removed = t.array(t.union(Types.RbxId, t.Instance)),
added = t.map(Types.RbxId, Types.ApiInstance),
updated = t.array(Types.ApiInstanceUpdate),
})
--[[
Invert the given PatchSet using the given instance map.
]]
function PatchSet.invert(patchSet, instanceMap)
error("not yet implemented", 2)
end
return PatchSet

View File

@@ -5,24 +5,12 @@
local RbxDom = require(script.Parent.Parent.RbxDom) local RbxDom = require(script.Parent.Parent.RbxDom)
local t = require(script.Parent.Parent.t) local t = require(script.Parent.Parent.t)
local Log = require(script.Parent.Parent.Log)
local Types = require(script.Parent.Types) local Types = require(script.Parent.Types)
local invariant = require(script.Parent.invariant) local invariant = require(script.Parent.invariant)
local getCanonicalProperty = require(script.Parent.getCanonicalProperty) local getCanonicalProperty = require(script.Parent.getCanonicalProperty)
local setCanonicalProperty = require(script.Parent.setCanonicalProperty) local setCanonicalProperty = require(script.Parent.setCanonicalProperty)
local PatchSet = require(script.Parent.PatchSet)
--[[
This interface represents either a patch created by the hydrate method, or a
patch returned from the API.
This type should be a subset of Types.ApiInstanceUpdate.
]]
local IPatch = t.interface({
removed = t.array(t.union(Types.RbxId, t.Instance)),
added = t.map(Types.RbxId, Types.ApiInstance),
updated = t.array(Types.ApiInstanceUpdate),
})
--[[ --[[
Attempt to safely set the parent of an instance. Attempt to safely set the parent of an instance.
@@ -86,7 +74,7 @@ end
editable by scripts. editable by scripts.
]] ]]
local applyPatchSchema = Types.ifEnabled(t.tuple( local applyPatchSchema = Types.ifEnabled(t.tuple(
IPatch PatchSet.validate
)) ))
function Reconciler:applyPatch(patch) function Reconciler:applyPatch(patch)
assert(applyPatchSchema(patch)) assert(applyPatchSchema(patch))
@@ -287,7 +275,7 @@ local hydrateSchema = Types.ifEnabled(t.tuple(
t.map(Types.RbxId, Types.VirtualInstance), t.map(Types.RbxId, Types.VirtualInstance),
Types.RbxId, Types.RbxId,
t.Instance, t.Instance,
IPatch PatchSet.validate
)) ))
function Reconciler:__hydrateInternal(apiInstances, id, instance, hydratePatch) function Reconciler:__hydrateInternal(apiInstances, id, instance, hydratePatch)
assert(hydrateSchema(apiInstances, id, instance, hydratePatch)) assert(hydrateSchema(apiInstances, id, instance, hydratePatch))

View File

@@ -1,8 +1,9 @@
local StudioService = game:GetService("StudioService")
local Log = require(script.Parent.Parent.Log) local Log = require(script.Parent.Parent.Log)
local Fmt = require(script.Parent.Parent.Fmt) local Fmt = require(script.Parent.Parent.Fmt)
local t = require(script.Parent.Parent.t) local t = require(script.Parent.Parent.t)
local DevSettings = require(script.Parent.DevSettings)
local InstanceMap = require(script.Parent.InstanceMap) local InstanceMap = require(script.Parent.InstanceMap)
local Reconciler = require(script.Parent.Reconciler) local Reconciler = require(script.Parent.Reconciler)
local strict = require(script.Parent.strict) local strict = require(script.Parent.strict)
@@ -43,6 +44,8 @@ ServeSession.Status = Status
local validateServeOptions = t.strictInterface({ local validateServeOptions = t.strictInterface({
apiContext = t.table, apiContext = t.table,
openScriptsExternally = t.boolean,
twoWaySync = t.boolean,
}) })
function ServeSession.new(options) function ServeSession.new(options)
@@ -57,12 +60,28 @@ function ServeSession.new(options)
local instanceMap = InstanceMap.new(onInstanceChanged) local instanceMap = InstanceMap.new(onInstanceChanged)
local reconciler = Reconciler.new(instanceMap) local reconciler = Reconciler.new(instanceMap)
local connections = {}
local connection = StudioService
:GetPropertyChangedSignal("ActiveScript")
:Connect(function()
local activeScript = StudioService.ActiveScript
if activeScript ~= nil then
self:__onActiveScriptChanged(activeScript)
end
end)
table.insert(connections, connection)
self = { self = {
__status = Status.NotStarted, __status = Status.NotStarted,
__apiContext = options.apiContext, __apiContext = options.apiContext,
__openScriptsExternally = options.openScriptsExternally,
__twoWaySync = options.twoWaySync,
__reconciler = reconciler, __reconciler = reconciler,
__instanceMap = instanceMap, __instanceMap = instanceMap,
__statusChangedCallback = nil, __statusChangedCallback = nil,
__connections = connections,
} }
setmetatable(self, ServeSession) setmetatable(self, ServeSession)
@@ -108,8 +127,39 @@ function ServeSession:stop()
self:__stopInternal() self:__stopInternal()
end end
function ServeSession:__onActiveScriptChanged(activeScript)
if not self.__openScriptsExternally then
Log.trace("Not opening script {} because feature not enabled.", activeScript)
return
end
if self.__status ~= Status.Connected then
Log.trace("Not opening script {} because session is not connected.", activeScript)
return
end
local scriptId = self.__instanceMap.fromInstances[activeScript]
if scriptId == nil then
Log.trace("Not opening script {} because it is not known by Rojo.", activeScript)
return
end
Log.debug("Trying to open script {} externally...", activeScript)
-- Force-close the script inside Studio
local existingParent = activeScript.Parent
activeScript.Parent = nil
activeScript.Parent = existingParent
-- Notify the Rojo server to open this script
self.__apiContext:open(scriptId)
end
function ServeSession:__onInstanceChanged(instance, propertyName) function ServeSession:__onInstanceChanged(instance, propertyName)
if not DevSettings:twoWaySyncEnabled() then if not self.__twoWaySync then
return return
end end
@@ -200,6 +250,11 @@ function ServeSession:__stopInternal(err)
self:__setStatus(Status.Disconnected, err) self:__setStatus(Status.Disconnected, err)
self.__apiContext:disconnect() self.__apiContext:disconnect()
self.__instanceMap:stop() self.__instanceMap:stop()
for _, connection in ipairs(self.__connections) do
connection:Disconnect()
end
self.__connections = {}
end end
function ServeSession:__setStatus(status, detail) function ServeSession:__setStatus(status, detail)

View File

@@ -1,14 +0,0 @@
local strict = require(script.Parent.strict)
return strict("Theme", {
ButtonFont = Enum.Font.GothamSemibold,
InputFont = Enum.Font.Code,
TitleFont = Enum.Font.GothamBold,
MainFont = Enum.Font.Gotham,
AccentColor = Color3.fromRGB(225, 56, 53),
AccentLightColor = Color3.fromRGB(255, 146, 145),
PrimaryColor = Color3.fromRGB(64, 64, 64),
SecondaryColor = Color3.fromRGB(235, 235, 235),
LightTextColor = Color3.fromRGB(160, 160, 160),
})

View File

@@ -0,0 +1,53 @@
--[[
Create a new signal that can be connected to, disconnected from, and fired.
Usage:
local signal = createSignal()
local disconnect = signal:connect(function(...)
print("fired:", ...)
end)
signal:fire("a", "b", "c")
disconnect()
Avoids mutating listeners list directly to prevent iterator invalidation if
a listener is disconnected while the signal is firing.
]]
local function createSignal()
local listeners = {}
local function connect(newListener)
local nextListeners = {}
for listener in pairs(listeners) do
nextListeners[listener] = true
end
nextListeners[newListener] = true
listeners = nextListeners
return function()
local nextListeners = {}
for listener in pairs(listeners) do
if listener ~= newListener then
nextListeners[listener] = true
end
end
listeners = nextListeners
end
end
local function fire(...)
for listener in pairs(listeners) do
listener(...)
end
end
return {
connect = connect,
fire = fire,
}
end
return createSignal

View File

@@ -1,7 +1,7 @@
local RbxDom = require(script.Parent.Parent.RbxDom) local RbxDom = require(script.Parent.Parent.RbxDom)
--[[ --[[
Attempts to set a property on the given instance. Attempts to read a property from the given instance.
]] ]]
local function getCanonincalProperty(instance, propertyName) local function getCanonincalProperty(instance, propertyName)
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName) local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)

View File

@@ -14,12 +14,20 @@ local Roact = require(script.Parent.Roact)
local Config = require(script.Config) local Config = require(script.Config)
local App = require(script.Components.App) local App = require(script.Components.App)
local Theme = require(script.Components.Theme)
local PluginSettings = require(script.Components.PluginSettings)
local app = Roact.createElement(App, { local app = Roact.createElement(Theme.StudioProvider, nil, {
plugin = plugin, Roact.createElement(PluginSettings.StudioProvider, {
plugin = plugin,
}, {
RojoUI = Roact.createElement(App, {
plugin = plugin,
}),
})
}) })
local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI") local tree = Roact.mount(app, nil, "Rojo UI")
plugin.Unloading:Connect(function() plugin.Unloading:Connect(function()
Roact.unmount(tree) Roact.unmount(tree)

View File

@@ -11,7 +11,7 @@ default = []
unstable_glob_ignore_paths = [] unstable_glob_ignore_paths = []
[dependencies] [dependencies]
env_logger = "0.6.2" env_logger = "0.7.1"
insta = { version = "0.13.1", features = ["redactions"] } insta = { version = "0.13.1", features = ["redactions"] }
log = "0.4.8" log = "0.4.8"
paste = "0.1.5" paste = "0.1.5"

View File

@@ -0,0 +1,28 @@
---
source: rojo-test/src/build_test.rs
expression: contents
---
<roblox version="4">
<Item class="DataModel" referent="0">
<Properties>
<string name="Name">infer-service-name</string>
</Properties>
<Item class="HttpService" referent="1">
<Properties>
<string name="Name">HttpService</string>
<bool name="HttpEnabled">true</bool>
</Properties>
</Item>
<Item class="ReplicatedStorage" referent="2">
<Properties>
<string name="Name">ReplicatedStorage</string>
</Properties>
<Item class="ModuleScript" referent="3">
<Properties>
<string name="Name">Main</string>
<string name="Source">-- hello, from main</string>
</Properties>
</Item>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,26 @@
---
source: rojo-test/src/build_test.rs
expression: contents
---
<roblox version="4">
<Item class="DataModel" referent="0">
<Properties>
<string name="Name">infer-service-name</string>
</Properties>
<Item class="StarterPlayer" referent="1">
<Properties>
<string name="Name">StarterPlayer</string>
</Properties>
<Item class="StarterCharacterScripts" referent="2">
<Properties>
<string name="Name">StarterCharacterScripts</string>
</Properties>
</Item>
<Item class="StarterPlayerScripts" referent="3">
<Properties>
<string name="Name">StarterPlayerScripts</string>
</Properties>
</Item>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,23 @@
---
source: rojo-test/src/build_test.rs
expression: contents
---
<roblox version="4">
<Item class="ModuleScript" referent="0">
<Properties>
<string name="Name">json_as_lua</string>
<string name="Source">return {
["1invalidident"] = "nice",
array = {1, 2, 3},
["false"] = false,
float = 1234.5452,
int = 1234,
null = nil,
object = {
hello = "world",
},
["true"] = true,
}</string>
</Properties>
</Item>
</roblox>

View File

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

View File

@@ -0,0 +1,16 @@
{
"name": "infer-service-name",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"Main": {
"$path": "main.lua"
}
},
"HttpService": {
"$properties": {
"HttpEnabled": true
}
}
}
}

View File

@@ -0,0 +1 @@
-- hello, from main

View File

@@ -0,0 +1,11 @@
{
"name": "infer-service-name",
"tree": {
"$className": "DataModel",
"StarterPlayer": {
"StarterPlayerScripts": {},
"StarterCharacterScripts": {}
}
}
}

View File

@@ -0,0 +1 @@
-- hello, from main

View File

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

View File

@@ -0,0 +1,6 @@
{
"name": "json_as_lua",
"tree": {
"$path": "make-me-a-script.json"
}
}

View File

@@ -0,0 +1,12 @@
{
"array": [1, 2, 3],
"object": {
"hello": "world"
},
"true": true,
"false": false,
"null": null,
"int": 1234,
"float": 1234.5452,
"1invalidident": "nice"
}

View File

@@ -1 +0,0 @@
This is a bare text file with no project.

View File

@@ -0,0 +1,6 @@
{
"name": "rbxmx_ref",
"tree": {
"$path": "model.rbxmx"
}
}

View File

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

View File

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

View File

@@ -1 +0,0 @@
Hello, world!

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More