Compare commits

..

9 Commits

Author SHA1 Message Date
Lucien Greathouse
eddc469f95 Release 6.2.0 2021-06-10 00:45:10 -04:00
Lucien Greathouse
21a4667fe4 Fix failing snapshot 2021-06-10 00:44:53 -04:00
Lucien Greathouse
b25f2fcd5d Update dependencies 2021-06-10 00:41:35 -04:00
Lucien Greathouse
0f7c9493d2 Fix 'Open Scripts Externally' crashing studio.
Closes #369.
2021-04-23 17:08:11 -04:00
Lucien Greathouse
f1c4102d7f Update changelog 2021-04-23 16:00:45 -04:00
Lucien Greathouse
8b5bfd5f44 Mark two-way sync as experimental in UI 2021-04-23 15:59:31 -04:00
Lucien Greathouse
0599b50235 Release 6.1.0 2021-04-12 17:19:35 -04:00
Lucien Greathouse
21f7ef6186 Update dependencies 2021-04-09 18:44:03 -04:00
MSAA
de6470bb45 change server bind address (#403)
* web/mod.rs - change server bind address

127.0.0.1 is a loopback interface, and only works on the same host
0.0.0.0 will allow connections from other hosts

ideally, this should be a console arg - but it's a quick fix

* implement --address option, revert default bind address to 127.0.0.1

* revert silly autoformatting

* ok, actually using rustfmt now

* More precise --address flag description

* Use SocketAddr where available, take advantage of const-ness

* Display 'localhost' if address is loopback

* Update Changelog

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2021-03-31 16:44:10 -04:00
719 changed files with 27042 additions and 127549 deletions

View File

@@ -1,2 +0,0 @@
((nil . ((eglot-luau-rojo-project-path . "plugin.project.json")
(eglot-luau-rojo-sourcemap-enabled . 't))))

View File

@@ -24,6 +24,3 @@ insert_final_newline = true
[*.lua] [*.lua]
indent_style = tab indent_style = tab
[*.luau]
indent_style = tab

View File

@@ -1,2 +0,0 @@
# stylua formatting
0f8e1625d572a5fe0f7b5c08653ff92cc837d346

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
*.lua linguist-language=Luau

View File

@@ -1,23 +0,0 @@
name: Changelog Check
on:
pull_request:
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
branches:
- master
jobs:
build:
name: Check Actions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Changelog check
uses: Zomzog/changelog-checker@v1.3.0
with:
fileName: CHANGELOG.md
noChangelogLabel: skip changelog
checkNotification: Simple
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -11,123 +11,29 @@ on:
jobs: jobs:
build: build:
name: Build and Test
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm] rust_version: [stable, "1.43.1"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
submodules: true submodules: true
- name: Install Rust - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable run: rustup default ${{ matrix.rust_version }}
- name: Restore Rust Cache
uses: actions/cache/restore@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
- name: Test - name: Run tests
run: cargo test --locked --verbose run: cargo test --locked --verbose
- name: Save Rust Cache - name: Rustfmt and Clippy
uses: actions/cache/save@v4 run: |
with: cargo fmt -- --check
path: | cargo clippy
~/.cargo/registry if: matrix.rust_version == 'stable'
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
msrv:
name: Check MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install Rust
uses: dtolnay/rust-toolchain@1.88.0
- name: Restore Rust Cache
uses: actions/cache/restore@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build
run: cargo build --locked --verbose
- name: Save Rust Cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
lint:
name: Rustfmt, Clippy, Stylua, & Selene
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Restore Rust Cache
uses: actions/cache/restore@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup Rokit
uses: CompeyDev/setup-rokit@v0.1.2
with:
version: 'v1.1.0'
- name: Stylua
run: stylua --check plugin/src
- name: Selene
run: selene plugin/src
- name: Rustfmt
run: cargo fmt -- --check
- name: Clippy
run: cargo clippy
- name: Save Rust Cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

View File

@@ -2,149 +2,65 @@ name: Release
on: on:
push: push:
tags: ["v*"] tags: ["*"]
jobs: jobs:
create-release: windows:
name: Create Release runs-on: windows-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }}
build-plugin:
needs: ["create-release"]
name: Build Roblox Studio Plugin
runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
submodules: true submodules: true
- name: Setup Rokit - name: Build release binary
uses: CompeyDev/setup-rokit@v0.1.2 run: cargo build --verbose --locked --release
- name: Upload artifacts
uses: actions/upload-artifact@v1
with: with:
version: 'v1.1.0' name: rojo-win64
path: target/release/rojo.exe
- name: Build Plugin macos:
run: rojo build plugin.project.json --output Rojo.rbxm runs-on: macos-latest
- name: Upload Plugin to Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ github.ref_name }} Rojo.rbxm
- name: Upload Plugin to Artifacts
uses: actions/upload-artifact@v4
with:
name: Rojo.rbxm
path: Rojo.rbxm
build:
needs: ["create-release"]
strategy:
fail-fast: false
matrix:
# https://doc.rust-lang.org/rustc/platform-support.html
include:
- host: linux
os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
label: linux-x86_64
- host: linux
os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
label: linux-aarch64
- host: windows
os: windows-latest
target: x86_64-pc-windows-msvc
label: windows-x86_64
- host: windows
os: windows-11-arm
target: aarch64-pc-windows-msvc
label: windows-aarch64
- host: macos
os: macos-latest
target: x86_64-apple-darwin
label: macos-x86_64
- host: macos
os: macos-latest
target: aarch64-apple-darwin
label: macos-aarch64
name: Build (${{ matrix.target }})
runs-on: ${{ matrix.os }}
env:
BIN: rojo
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
with:
targets: ${{ matrix.target }}
- name: Restore Rust Cache - name: Build release binary
uses: actions/cache/restore@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build Release
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
- name: Save Rust Cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Generate Artifact Name
shell: bash
env:
TAG_NAME: ${{ github.ref_name }}
run: | run: |
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV" source $HOME/.cargo/env
cargo build --verbose --locked --release
- name: Create Archive and Upload to Release
shell: bash
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OPENSSL_STATIC: 1
run: |
mkdir staging
if [ "${{ matrix.host }}" = "windows" ]; then - name: Upload artifacts
cp "target/${{ matrix.target }}/release/$BIN.exe" staging/ uses: actions/upload-artifact@v1
cd staging
7z a ../$ARTIFACT_NAME *
else
cp "target/${{ matrix.target }}/release/$BIN" staging/
cd staging
zip ../$ARTIFACT_NAME *
fi
gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME
- name: Upload Archive to Artifacts
uses: actions/upload-artifact@v4
with: with:
path: ${{ env.ARTIFACT_NAME }} name: rojo-macos
name: ${{ env.ARTIFACT_NAME }} path: target/release/rojo
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Build
run: cargo build --locked --verbose --release
env:
OPENSSL_STATIC: 1
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: rojo-linux
path: target/release/rojo

12
.gitignore vendored
View File

@@ -10,8 +10,8 @@
/*.rbxl /*.rbxl
/*.rbxlx /*.rbxlx
# Sourcemap for the Rojo plugin (for better intellisense) # Test places for the Roblox Studio Plugin
/sourcemap.json /plugin/*.rbxlx
# Roblox Studio holds 'lock' files on places # Roblox Studio holds 'lock' files on places
*.rbxl.lock *.rbxl.lock
@@ -20,9 +20,5 @@
# Snapshot files from the 'insta' Rust crate # Snapshot files from the 'insta' Rust crate
**/*.snap.new **/*.snap.new
# Macos file system junk # Selene generates a roblox.toml file that should not be checked in.
._* /roblox.toml
.DS_STORE
# JetBrains IDEs
/.idea/

34
.gitmodules vendored
View File

@@ -1,21 +1,15 @@
[submodule "plugin/Packages/Roact"] [submodule "plugin/modules/roact"]
path = plugin/Packages/Roact path = plugin/modules/roact
url = https://github.com/roblox/roact.git url = https://github.com/Roblox/roact.git
[submodule "plugin/Packages/Flipper"] [submodule "plugin/modules/testez"]
path = plugin/Packages/Flipper path = plugin/modules/testez
url = https://github.com/reselim/flipper.git url = https://github.com/Roblox/testez.git
[submodule "plugin/Packages/Promise"] [submodule "plugin/modules/promise"]
path = plugin/Packages/Promise path = plugin/modules/promise
url = https://github.com/evaera/roblox-lua-promise.git url = https://github.com/LPGhatguy/roblox-lua-promise.git
[submodule "plugin/Packages/t"] [submodule "plugin/modules/t"]
path = plugin/Packages/t path = plugin/modules/t
url = https://github.com/osyrisrblx/t.git url = https://github.com/osyrisrblx/t.git
[submodule "plugin/Packages/TestEZ"] [submodule "plugin/modules/flipper"]
path = plugin/Packages/TestEZ path = plugin/modules/flipper
url = https://github.com/roblox/testez.git url = https://github.com/Reselim/Flipper
[submodule "plugin/Packages/Highlighter"]
path = plugin/Packages/Highlighter
url = https://github.com/boatbomber/highlighter.git
[submodule "plugin/Packages/msgpack-luau"]
path = plugin/Packages/msgpack-luau
url = https://github.com/cipharius/msgpack-luau/

58
.luacheckrc Normal file
View File

@@ -0,0 +1,58 @@
stds.roblox = {
read_globals = {
game = {
other_fields = true,
},
-- Roblox globals
"script",
-- Extra functions
"tick", "warn", "spawn",
"wait", "settings", "typeof",
-- Types
"Vector2", "Vector3",
"Vector2int16", "Vector3int16",
"Color3",
"UDim", "UDim2",
"Rect",
"CFrame",
"Enum",
"Instance",
"DockWidgetPluginGuiInfo",
}
}
stds.plugin = {
read_globals = {
"plugin",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP", "itFIXME",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
"421", -- shadowing local variable
"422", -- shadowing argument
"431", -- shadowing upvalue
"432", -- shadowing upvalue argument
}
std = "lua51+roblox"
files["**/*.server.lua"] = {
std = "+plugin",
}
files["**/*.spec.lua"] = {
std = "+testez",
}

View File

@@ -1,8 +0,0 @@
{
"recommendations": [
"JohnnyMorganz.luau-lsp",
"JohnnyMorganz.stylua",
"Kampfkarren.selene-vscode",
"rust-lang.rust-analyzer"
]
}

View File

@@ -1,4 +0,0 @@
{
"luau-lsp.sourcemap.rojoProjectFile": "plugin.project.json",
"luau-lsp.sourcemap.autogenerate": true
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,31 +14,13 @@ Code contributions are welcome for features and bugs that have been reported in
You'll want these tools to work on Rojo: You'll want these tools to work on Rojo:
* Latest stable Rust compiler * Latest stable Rust compiler
* Rustfmt and Clippy are used for code formatting and linting.
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo) * Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
* [Rokit](https://github.com/rojo-rbx/rokit) * [Foreman](https://github.com/Roblox/foreman)
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
When working on the Studio plugin, we recommend using this command to automatically rebuild the plugin when you save a change:
*(Make sure you've enabled the Studio setting to reload plugins on file change!)*
```bash
bash scripts/watch-build-plugin.sh
```
You can also run the plugin's unit tests with the following:
*(Make sure you have `run-in-roblox` installed first!)*
```bash
bash scripts/unit-test-plugin.sh
```
## Documentation ## Documentation
Documentation impacts way more people than the individual lines of code we write. Documentation impacts way more people than the individual lines of code we write.
If you find any problems in the documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them. If you find any problems in documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
## Bug Reports and Feature Requests ## Bug Reports and Feature Requests
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right. Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.
@@ -47,29 +29,25 @@ Sometimes there's something that Rojo doesn't do that it probably should.
Please file issues and we'll try to help figure out what the best way forward is. Please file issues and we'll try to help figure out what the best way forward is.
## Local Development Gotchas
If your build fails with "Error: failed to open file `D:\code\rojo\plugin\modules\roact\src`" you need to update your Git submodules.
Run the command and try building again: `git submodule update --init --recursive`.
## 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 [`Cargo.toml`](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 run 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 6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
* `git tag vX.Y.Z`
7. Publish the CLI 7. Publish the CLI
* `cargo publish` * `cargo publish`
8. Publish the Plugin 8. Build and upload the plugin
* `cargo run -- upload plugin --asset_id 6415005344` * `rojo build plugin -o Rojo.rbxm`
* Upload `Rojo.rbxm` to Roblox.com, keep it for later
9. Push commits and tags 9. Push commits and tags
* `git push && git push --tags` * `git push && git push --tags`
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

3526
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,18 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.7.0-rc.1" version = "6.2.0"
rust-version = "1.88" authors = ["Lucien Greathouse <me@lpghatguy.com>"]
authors = [
"Lucien Greathouse <me@lpghatguy.com>",
"Micah Reid <git@dekkonot.com>",
"Ken Loeffler <kenloef@gmail.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"
homepage = "https://rojo.space" homepage = "https://rojo.space"
documentation = "https://rojo.space/docs" documentation = "https://rojo.space/docs"
repository = "https://github.com/rojo-rbx/rojo" repository = "https://github.com/rojo-rbx/rojo"
readme = "README.md" readme = "README.md"
edition = "2021" edition = "2018"
build = "build.rs"
exclude = ["/test-projects/**"] exclude = [
"/test-projects/**",
]
[profile.dev] [profile.dev]
panic = "abort" panic = "abort"
@@ -30,104 +26,80 @@ default = []
# 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 = []
# Run Rojo with this feature to open a Tracy session.
# Currently uses protocol v63, last supported in Tracy 0.9.1.
profile-with-tracy = ["profiling/profile-with-tracy"]
[workspace] [workspace]
members = ["crates/*"] members = [
"rojo-insta-ext",
"memofs",
]
[lib] [lib]
name = "librojo" name = "librojo"
path = "src/lib.rs" path = "src/lib.rs"
[[bin]]
name = "rojo"
path = "src/bin.rs"
[[bench]] [[bench]]
name = "build" name = "build"
harness = false harness = false
[dependencies] [dependencies]
memofs = { version = "0.3.1", path = "crates/memofs" } memofs = { version = "0.1.2", path = "memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously anyhow = "1.0.27"
# rbx_binary = { path = "../rbx-dom/rbx_binary", features = [ backtrace = "0.3"
# "unstable_text_format", bincode = "1.2.1"
# ] } crossbeam-channel = "0.4.0"
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" } csv = "1.1.1"
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" } env_logger = "0.7.1"
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" } fs-err = "2.2.0"
# rbx_xml = { path = "../rbx-dom/rbx_xml" } futures = "0.1.29"
globset = "0.4.4"
rbx_binary = { version = "2.0.1", features = ["unstable_text_format"] } humantime = "1.3.0"
rbx_dom_weak = "4.1.0" hyper = "0.12.35"
rbx_reflection = "6.1.0" jod-thread = "0.1.0"
rbx_reflection_database = "2.0.2" lazy_static = "1.4.0"
rbx_xml = "2.0.1" log = "0.4.8"
maplit = "1.0.1"
anyhow = "1.0.80" notify = "4.0.14"
backtrace = "0.3.69" opener = "0.4.1"
bincode = "1.3.3" rbx_binary = "0.5.0"
crossbeam-channel = "0.5.12" rbx_dom_weak = "1.10.1"
csv = "1.3.0" rbx_reflection = "3.3.408"
env_logger = "0.9.3" rbx_xml = "0.11.3"
fs-err = "2.11.0" regex = "1.3.1"
futures = "0.3.30" reqwest = "0.9.20"
globset = "0.4.14"
humantime = "2.1.0"
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
hyper-tungstenite = "0.11.0"
jod-thread = "0.1.2"
log = "0.4.21"
num_cpus = "1.16.0"
opener = "0.5.2"
rayon = "1.9.0"
reqwest = { version = "0.11.24", default-features = false, features = [
"blocking",
"json",
"rustls-tls",
] }
ritz = "0.1.0" ritz = "0.1.0"
roblox_install = "1.0.0" rlua = "0.17.0"
serde = { version = "1.0.197", features = ["derive", "rc"] } roblox_install = "0.2.2"
serde_json = "1.0.145" serde = { version = "1.0", features = ["derive", "rc"] }
jsonc-parser = { version = "0.27.0", features = ["serde"] } serde_json = "1.0"
strum = { version = "0.27", features = ["derive"] } structopt = "0.3.5"
toml = "0.5.11" termcolor = "1.0.5"
termcolor = "1.4.1" thiserror = "1.0.11"
thiserror = "1.0.57" tokio = "0.1.22"
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] } uuid = { version = "0.8.1", features = ["v4", "serde"] }
uuid = { version = "1.7.0", features = ["v4", "serde"] }
clap = { version = "3.2.25", features = ["derive"] }
profiling = "1.0.15"
yaml-rust2 = "0.10.3"
data-encoding = "2.8.0"
pathdiff = "0.2.3"
blake3 = "1.5.0"
float-cmp = "0.9.0"
indexmap = { version = "2.10.0", features = ["serde"] }
rmp-serde = "1.3.0"
serde_bytes = "0.11.19"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.10.1" winreg = "0.6.2"
[build-dependencies] [build-dependencies]
memofs = { version = "0.3.0", path = "crates/memofs" } memofs = { version = "0.1.3", path = "memofs" }
embed-resource = "1.8.0" anyhow = "1.0.27"
anyhow = "1.0.80" bincode = "1.2.1"
bincode = "1.3.3" fs-err = "2.3.0"
fs-err = "2.11.0" maplit = "1.0.1"
maplit = "1.0.2"
semver = "1.0.22"
[dev-dependencies] [dev-dependencies]
rojo-insta-ext = { path = "crates/rojo-insta-ext" } rojo-insta-ext = { path = "rojo-insta-ext" }
criterion = "0.3.6" criterion = "0.3"
insta = { version = "1.36.1", features = ["redactions", "yaml", "json"] } insta = { version = "1.3.0", features = ["redactions"] }
paste = "1.0.14" lazy_static = "1.2"
pretty_assertions = "1.4.0" paste = "0.1"
serde_yaml = "0.8.26" pretty_assertions = "0.6.1"
tempfile = "3.10.1" serde_yaml = "0.8.9"
walkdir = "2.5.0" tempfile = "3.0"
walkdir = "2.1"

View File

@@ -1,13 +1,21 @@
<div align="center"> <div align="center">
<a href="https://rojo.space"><img src="assets/brand_images/logo-512.png" alt="Rojo" height="217" /></a> <a href="https://rojo.space">
<img src="assets/logo-512.png" alt="Rojo" height="217" />
</a>
</div> </div>
<div>&nbsp;</div> <div>&nbsp;</div>
<div align="center"> <div align="center">
<a href="https://github.com/rojo-rbx/rojo/actions"><img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" /></a> <a href="https://github.com/rojo-rbx/rojo/actions">
<a href="https://crates.io/crates/rojo"><img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" /></a> <img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" />
<a href="https://rojo.space/docs"><img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" /></a> </a>
<a href="https://crates.io/crates/rojo">
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
</a>
<a href="https://rojo.space/docs">
<img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" />
</a>
</div> </div>
<hr /> <hr />
@@ -40,7 +48,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome! Pull requests are welcome!
Rojo supports Rust 1.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has. Rojo supports Rust 1.43.1 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
## License ## License
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details. Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -2,7 +2,7 @@
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}. Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started ## Getting Started
To build this library, use: To build this library or plugin, use:
```bash ```bash
rojo build -o "{project_name}.rbxmx" rojo build -o "{project_name}.rbxmx"

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 975 B

After

Width:  |  Height:  |  Size: 975 B

View File

Before

Width:  |  Height:  |  Size: 229 B

After

Width:  |  Height:  |  Size: 229 B

View File

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 584 B

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 B

View File

@@ -17,10 +17,6 @@ html {
line-height: 1.4; line-height: 1.4;
} }
body {
background-color: #e7e7e7
}
img { img {
max-width:100%; max-width:100%;
max-height:100%; max-height:100%;

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

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

View File

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

View File

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

View File

@@ -1,17 +0,0 @@
# {project_name}
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started
To build this plugin to your local plugins folder, use:
```bash
rojo build -p "{project_name}.rbxm"
```
You can include the `watch` flag to re-build it on save:
```bash
rojo build -p "{project_name}.rbxm" --watch
```
For more help, check out [the Rojo documentation](https://rojo.space/docs).

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use librojo::cli::BuildCommand; use librojo::cli::{build, BuildCommand};
pub fn benchmark_small_place(c: &mut Criterion) { pub fn benchmark_small_place(c: &mut Criterion) {
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place") bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
@@ -20,7 +20,7 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
group.bench_function("build", |b| { group.bench_function("build", |b| {
b.iter_batched( b.iter_batched(
|| place_setup(path), || place_setup(path),
|(_dir, options)| options.run().unwrap(), |(_dir, options)| build(options).unwrap(),
BatchSize::SmallInput, BatchSize::SmallInput,
) )
}); });
@@ -31,12 +31,11 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) { fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let input = input_path.as_ref().to_path_buf(); let input = input_path.as_ref().to_path_buf();
let output = Some(dir.path().join("output.rbxlx")); let output = dir.path().join("output.rbxlx");
let options = BuildCommand { let options = BuildCommand {
project: input, project: input,
watch: false, watch: false,
plugin: None,
output, output,
}; };

5
bin/dev-plugin.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -e
watchexec -c -w plugin "sh -c './bin/install-dev-plugin.sh'"

13
bin/install-dev-plugin.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
set -e
DIR="$( mktemp -d )"
PLUGIN_FILE="$DIR/Rojo.rbxm"
TESTEZ_FILE="$DIR/TestEZ.rbxm"
rojo build plugin -o "$PLUGIN_FILE"
rojo build plugin/testez.project.json -o "$TESTEZ_FILE"
remodel bin/mark-plugin-as-dev.lua "$PLUGIN_FILE" "$TESTEZ_FILE" 2>/dev/null
cp "$PLUGIN_FILE" "$LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm"

5
bin/install-release-plugin.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -e
rojo build plugin -o "$LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm"

View File

@@ -0,0 +1,12 @@
local pluginPath, testezPath = ...
local plugin = remodel.readModelFile(pluginPath)[1]
local testez = remodel.readModelFile(testezPath)[1]
local marker = Instance.new("Folder")
marker.Name = "ROJO_DEV_BUILD"
marker.Parent = plugin
testez.Parent = plugin
remodel.writeModelFile(plugin, pluginPath)

View File

@@ -0,0 +1,8 @@
local pluginPath, placePath = ...
local plugin = remodel.readModelFile(pluginPath)[1]
local place = remodel.readPlaceFile(placePath)
plugin.Parent = place:GetService("ReplicatedStorage")
remodel.writePlaceFile(place, placePath)

6
bin/run-all-tests.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
set -e
./bin/run-cli-tests.sh
./bin/run-plugin-tests.sh

9
bin/run-cli-tests.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -e
cargo test --all --locked
cargo fmt -- --check
touch src/lib.rs # Nudge Rust source to make Clippy actually check things
cargo clippy

16
bin/run-plugin-tests.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
set -e
DIR="$( mktemp -d )"
PLUGIN_FILE="$DIR/Rojo.rbxmx"
PLACE_FILE="$DIR/RojoTestPlace.rbxlx"
rojo build plugin -o "$PLUGIN_FILE"
rojo build plugin/place.project.json -o "$PLACE_FILE"
remodel bin/put-plugin-in-test-place.lua "$PLUGIN_FILE" "$PLACE_FILE"
run-in-roblox -s plugin/testBootstrap.server.lua "$PLACE_FILE"
luacheck plugin/src plugin/log plugin/http

View File

@@ -7,7 +7,6 @@ use fs_err as fs;
use fs_err::File; use fs_err::File;
use maplit::hashmap; use maplit::hashmap;
use memofs::VfsSnapshot; use memofs::VfsSnapshot;
use semver::Version;
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> { fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
println!("cargo:rerun-if-changed={}", path.display()); println!("cargo:rerun-if-changed={}", path.display());
@@ -20,18 +19,9 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
let file_name = entry.file_name().to_str().unwrap().to_owned(); let file_name = entry.file_name().to_str().unwrap().to_owned();
if file_name.starts_with(".git") {
continue;
}
// We can skip any TestEZ test files since they aren't necessary for // We can skip any TestEZ test files since they aren't necessary for
// the plugin to run. // the plugin to run.
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") { if file_name.ends_with(".spec.lua") {
continue;
}
// Ignore images in msgpack-luau because they aren't UTF-8 encoded.
if file_name.ends_with(".png") {
continue; continue;
} }
@@ -50,43 +40,38 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
fn main() -> Result<(), anyhow::Error> { fn main() -> Result<(), anyhow::Error> {
let out_dir = env::var_os("OUT_DIR").unwrap(); let out_dir = env::var_os("OUT_DIR").unwrap();
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
let plugin_dir = root_dir.join("plugin"); let plugin_root = PathBuf::from(root_dir).join("plugin");
let templates_dir = root_dir.join("assets").join("project-templates");
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?; let plugin_modules = plugin_root.join("modules");
let plugin_version =
Version::parse(fs::read_to_string(plugin_dir.join("Version.txt"))?.trim())?;
assert_eq!( let snapshot = VfsSnapshot::dir(hashmap! {
our_version, plugin_version, "default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
"plugin version does not match Cargo version" "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"))?,
let template_snapshot = snapshot_from_fs_path(&templates_dir)?; "rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
let plugin_snapshot = VfsSnapshot::dir(hashmap! { "modules" => VfsSnapshot::dir(hashmap! {
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?, "roact" => VfsSnapshot::dir(hashmap! {
"plugin" => VfsSnapshot::dir(hashmap! { "src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))?
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?, }),
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?, "promise" => VfsSnapshot::dir(hashmap! {
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?, "lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))?
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?, }),
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?, "t" => VfsSnapshot::dir(hashmap! {
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?, "lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?, }),
"flipper" => VfsSnapshot::dir(hashmap! {
"src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))?
}),
}), }),
}); });
let template_file = File::create(Path::new(&out_dir).join("templates.bincode"))?; let out_path = Path::new(&out_dir).join("plugin.bincode");
let plugin_file = File::create(Path::new(&out_dir).join("plugin.bincode"))?; let out_file = File::create(&out_path)?;
bincode::serialize_into(plugin_file, &plugin_snapshot)?; bincode::serialize_into(out_file, &snapshot)?;
bincode::serialize_into(template_file, &template_snapshot)?;
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");
embed_resource::compile("build/windows/rojo-manifest.rc");
Ok(()) Ok(())
} }

View File

@@ -1,2 +0,0 @@
#define RT_MANIFEST 24
1 RT_MANIFEST "rojo.manifest"

View File

@@ -1,8 +0,0 @@
<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<ws2:longPathAware>true</ws2:longPathAware>
</windowsSettings>
</application>
</assembly>

View File

@@ -1,33 +0,0 @@
# memofs Changelog
## Unreleased Changes
* Added `Vfs::canonicalize`. [#1201]
## 0.3.1 (2025-11-27)
* Added `Vfs::exists`. [#1169]
* Added `create_dir` and `create_dir_all` to allow creating directories. [#937]
[#1169]: https://github.com/rojo-rbx/rojo/pull/1169
[#937]: https://github.com/rojo-rbx/rojo/pull/937
## 0.3.0 (2024-03-15)
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
[#830]: https://github.com/rojo-rbx/rojo/pull/830
[#854]: https://github.com/rojo-rbx/rojo/pull/854
## 0.2.0 (2021-08-23)
* Updated to `crossbeam-channel` 0.5.1.
## 0.1.3 (2020-11-19)
* Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching.
## 0.1.2 (2020-03-29)
* `VfsSnapshot` now implements Serde's `Serialize` and `Deserialize` traits.
## 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,74 +0,0 @@
use std::io;
use std::path::{Path, PathBuf};
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
/// `VfsBackend` that returns an error on every operation.
#[non_exhaustive]
pub struct NoopBackend;
impl NoopBackend {
pub fn new() -> Self {
Self
}
}
impl VfsBackend for NoopBackend {
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn exists(&mut self, _path: &Path) -> io::Result<bool> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn create_dir(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn canonicalize(&mut self, _path: &Path) -> io::Result<PathBuf> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
crossbeam_channel::never()
}
fn watch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
}
impl Default for NoopBackend {
fn default() -> Self {
Self::new()
}
}

3
foreman.toml Normal file
View File

@@ -0,0 +1,3 @@
[tools]
rojo = { source = "rojo-rbx/rojo", version = "6.0.0-rc.3" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }

15
memofs/CHANGELOG.md Normal file
View File

@@ -0,0 +1,15 @@
# memofs Changelog
## Unreleased Changes
## 0.1.3 (2020-11-19)
* Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching.
## 0.1.2 (2020-03-29)
* `VfsSnapshot` now implements Serde's `Serialize` and `Deserialize` traits.
## 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,12 +1,8 @@
[package] [package]
name = "memofs" name = "memofs"
description = "Virtual filesystem with configurable backends." description = "Virtual filesystem with configurable backends."
version = "0.3.1" version = "0.1.3"
authors = [ authors = ["Lucien Greathouse <me@lpghatguy.com>"]
"Lucien Greathouse <me@lpghatguy.com>",
"Micah Reid <git@dekkonot.com>",
"Ken Loeffler <kenloef@gmail.com>",
]
edition = "2018" edition = "2018"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
@@ -15,10 +11,7 @@ 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]
crossbeam-channel = "0.5.12" crossbeam-channel = "0.4.0"
fs-err = "2.11.0" fs-err = "2.3.0"
notify = "4.0.17" notify = "4.0.15"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
tempfile = "3.10.1"

View File

@@ -50,12 +50,6 @@ impl InMemoryFs {
} }
} }
impl Default for InMemoryFs {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)] #[derive(Debug)]
struct InMemoryFsInner { struct InMemoryFsInner {
entries: HashMap<PathBuf, Entry>, entries: HashMap<PathBuf, Entry>,
@@ -157,11 +151,6 @@ impl VfsBackend for InMemoryFs {
) )
} }
fn exists(&mut self, path: &Path) -> io::Result<bool> {
let inner = self.inner.lock().unwrap();
Ok(inner.entries.contains_key(path))
}
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
let inner = self.inner.lock().unwrap(); let inner = self.inner.lock().unwrap();
@@ -181,21 +170,6 @@ impl VfsBackend for InMemoryFs {
} }
} }
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap();
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
}
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap();
let mut path_buf = path.to_path_buf();
while let Some(parent) = path_buf.parent() {
inner.load_snapshot(parent.to_path_buf(), VfsSnapshot::empty_dir())?;
path_buf.pop();
}
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> { fn remove_file(&mut self, path: &Path) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap(); let mut inner = self.inner.lock().unwrap();
@@ -232,33 +206,6 @@ impl VfsBackend for InMemoryFs {
} }
} }
// TODO: We rely on Rojo to prepend cwd to any relative path before storing paths
// in MemoFS. The current implementation will error if no prepended absolute path
// is found. It really only normalizes paths within the provided path's context.
// Example: "/Users/username/project/../other/file.txt" ->
// "/Users/username/other/file.txt"
// Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt"
// This is not very robust. We should implement proper path normalization here or otherwise
// warn if we are missing context and can not fully canonicalize the path correctly.
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
normalized.pop();
}
std::path::Component::CurDir => {}
_ => normalized.push(component),
}
}
let inner = self.inner.lock().unwrap();
match inner.entries.get(&normalized) {
Some(_) => Ok(normalized),
None => not_found(&normalized),
}
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
let inner = self.inner.lock().unwrap(); let inner = self.inner.lock().unwrap();
@@ -275,17 +222,23 @@ impl VfsBackend for InMemoryFs {
} }
fn must_be_file<T>(path: &Path) -> io::Result<T> { fn must_be_file<T>(path: &Path) -> io::Result<T> {
Err(io::Error::other(format!( Err(io::Error::new(
io::ErrorKind::Other,
format!(
"path {} was a directory, but must be a file", "path {} was a directory, but must be a file",
path.display() path.display()
))) ),
))
} }
fn must_be_dir<T>(path: &Path) -> io::Result<T> { fn must_be_dir<T>(path: &Path) -> io::Result<T> {
Err(io::Error::other(format!( Err(io::Error::new(
io::ErrorKind::Other,
format!(
"path {} was a file, but must be a directory", "path {} was a file, but must be a directory",
path.display() path.display()
))) ),
))
} }
fn not_found<T>(path: &Path) -> io::Result<T> { fn not_found<T>(path: &Path) -> io::Result<T> {

View File

@@ -22,9 +22,9 @@ mod noop_backend;
mod snapshot; mod snapshot;
mod std_backend; mod std_backend;
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};
use std::{io, str};
pub use in_memory_fs::InMemoryFs; pub use in_memory_fs::InMemoryFs;
pub use noop_backend::NoopBackend; pub use noop_backend::NoopBackend;
@@ -70,14 +70,10 @@ impl<T> IoResultExt<T> for io::Result<T> {
pub trait VfsBackend: sealed::Sealed + Send + 'static { pub trait VfsBackend: sealed::Sealed + Send + 'static {
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>; fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>; fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
fn exists(&mut self, path: &Path) -> io::Result<bool>;
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>; fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
fn create_dir(&mut self, path: &Path) -> io::Result<()>;
fn create_dir_all(&mut self, path: &Path) -> io::Result<()>;
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>; fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
fn remove_file(&mut self, path: &Path) -> io::Result<()>; fn remove_file(&mut self, path: &Path) -> io::Result<()>;
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>; fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf>;
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>; fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
fn watch(&mut self, path: &Path) -> io::Result<()>; fn watch(&mut self, path: &Path) -> io::Result<()>;
@@ -159,29 +155,6 @@ impl VfsInner {
Ok(Arc::new(contents)) Ok(Arc::new(contents))
} }
fn read_to_string<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<String>> {
let path = path.as_ref();
let contents = self.backend.read(path)?;
if self.watch_enabled {
self.backend.watch(path)?;
}
let contents_str = str::from_utf8(&contents).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("File was not valid UTF-8: {}", path.display()),
)
})?;
Ok(Arc::new(contents_str.into()))
}
fn exists<P: AsRef<Path>>(&mut self, path: P) -> io::Result<bool> {
let path = path.as_ref();
self.backend.exists(path)
}
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> { fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
let path = path.as_ref(); let path = path.as_ref();
let contents = contents.as_ref(); let contents = contents.as_ref();
@@ -199,16 +172,6 @@ impl VfsInner {
Ok(dir) Ok(dir)
} }
fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.backend.create_dir(path)
}
fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.backend.create_dir_all(path)
}
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> { fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref(); let path = path.as_ref();
let _ = self.backend.unwatch(path); let _ = self.backend.unwatch(path);
@@ -226,18 +189,16 @@ impl VfsInner {
self.backend.metadata(path) self.backend.metadata(path)
} }
fn canonicalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
self.backend.canonicalize(path)
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
self.backend.event_receiver() self.backend.event_receiver()
} }
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> { fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
if let VfsEvent::Remove(path) = event { match event {
let _ = self.backend.unwatch(path); VfsEvent::Remove(path) => {
let _ = self.backend.unwatch(&path);
}
_ => {}
} }
Ok(()) Ok(())
@@ -300,33 +261,6 @@ impl Vfs {
self.inner.lock().unwrap().read(path) self.inner.lock().unwrap().read(path)
} }
/// Read a file from the VFS (or from the underlying backend if it isn't
/// resident) into a string.
///
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string].
///
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
#[inline]
pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
let path = path.as_ref();
self.inner.lock().unwrap().read_to_string(path)
}
/// Read a file from the VFS (or the underlying backend if it isn't
/// resident) into a string, and normalize its line endings to LF.
///
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string], but also performs
/// line ending normalization.
///
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
#[inline]
pub fn read_to_string_lf_normalized<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
let path = path.as_ref();
let contents = self.inner.lock().unwrap().read_to_string(path)?;
Ok(contents.replace("\r\n", "\n").into())
}
/// Write a file to the VFS and the underlying backend. /// Write a file to the VFS and the underlying backend.
/// ///
/// Roughly equivalent to [`std::fs::write`][std::fs::write]. /// Roughly equivalent to [`std::fs::write`][std::fs::write].
@@ -350,42 +284,6 @@ impl Vfs {
self.inner.lock().unwrap().read_dir(path) self.inner.lock().unwrap().read_dir(path)
} }
/// Return whether the given path exists.
///
/// Roughly equivalent to [`std::fs::exists`][std::fs::exists].
///
/// [std::fs::exists]: https://doc.rust-lang.org/stable/std/fs/fn.exists.html
#[inline]
pub fn exists<P: AsRef<Path>>(&self, path: P) -> io::Result<bool> {
let path = path.as_ref();
self.inner.lock().unwrap().exists(path)
}
/// Creates a directory at the provided location.
///
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
/// Similiar to that function, this function will fail if the parent of the
/// path does not exist.
///
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
#[inline]
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.lock().unwrap().create_dir(path)
}
/// Creates a directory at the provided location, recursively creating
/// all parent components if they are missing.
///
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
///
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
#[inline]
pub fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.lock().unwrap().create_dir_all(path)
}
/// Remove a file. /// Remove a file.
/// ///
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
@@ -419,19 +317,6 @@ impl Vfs {
self.inner.lock().unwrap().metadata(path) self.inner.lock().unwrap().metadata(path)
} }
/// Normalize a path via the underlying backend.
///
/// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are
/// resolved against the backend's current working directory (if applicable) and errors are
/// surfaced directly from the backend.
///
/// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
#[inline]
pub fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
self.inner.lock().unwrap().canonicalize(path)
}
/// Retrieve a handle to the event receiver for this `Vfs`. /// Retrieve a handle to the event receiver for this `Vfs`.
#[inline] #[inline]
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
@@ -501,31 +386,6 @@ impl VfsLock<'_> {
self.inner.read_dir(path) self.inner.read_dir(path)
} }
/// Creates a directory at the provided location.
///
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
/// Similiar to that function, this function will fail if the parent of the
/// path does not exist.
///
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
#[inline]
pub fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.create_dir(path)
}
/// Creates a directory at the provided location, recursively creating
/// all parent components if they are missing.
///
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
///
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
#[inline]
pub fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.create_dir_all(path)
}
/// Remove a file. /// Remove a file.
/// ///
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
@@ -559,13 +419,6 @@ impl VfsLock<'_> {
self.inner.metadata(path) self.inner.metadata(path)
} }
/// Normalize a path via the underlying backend.
#[inline]
pub fn normalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
self.inner.canonicalize(path)
}
/// Retrieve a handle to the event receiver for this `Vfs`. /// Retrieve a handle to the event receiver for this `Vfs`.
#[inline] #[inline]
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
@@ -578,83 +431,3 @@ impl VfsLock<'_> {
self.inner.commit_event(event) self.inner.commit_event(event)
} }
} }
#[cfg(test)]
mod test {
use crate::{InMemoryFs, StdBackend, Vfs, VfsSnapshot};
use std::io;
use std::path::PathBuf;
/// https://github.com/rojo-rbx/rojo/issues/899
#[test]
fn read_to_string_lf_normalized_keeps_trailing_newline() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot("test", VfsSnapshot::file("bar\r\nfoo\r\n\r\n"))
.unwrap();
let vfs = Vfs::new(imfs);
assert_eq!(
vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
"bar\nfoo\n\n"
);
}
/// https://github.com/rojo-rbx/rojo/issues/1200
#[test]
fn canonicalize_in_memory_success() {
let mut imfs = InMemoryFs::new();
let contents = "Lorem ipsum dolor sit amet.".to_string();
imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string()))
.unwrap();
let vfs = Vfs::new(imfs);
assert_eq!(
vfs.canonicalize("/test/nested/../file.txt").unwrap(),
PathBuf::from("/test/file.txt")
);
assert_eq!(
vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap())
.unwrap()
.to_string(),
contents.to_string()
);
}
#[test]
fn canonicalize_in_memory_missing_errors() {
let imfs = InMemoryFs::new();
let vfs = Vfs::new(imfs);
let err = vfs.canonicalize("test").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn canonicalize_std_backend_success() {
let contents = "Lorem ipsum dolor sit amet.".to_string();
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("file.txt");
fs_err::write(&file_path, contents.to_string()).unwrap();
let vfs = Vfs::new(StdBackend::new());
let canonicalized = vfs.canonicalize(&file_path).unwrap();
assert_eq!(canonicalized, file_path.canonicalize().unwrap());
assert_eq!(
vfs.read_to_string(&canonicalized).unwrap().to_string(),
contents.to_string()
);
}
#[test]
fn canonicalize_std_backend_missing_errors() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test");
let vfs = Vfs::new(StdBackend::new());
let err = vfs.canonicalize(&file_path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
}

View File

@@ -0,0 +1,76 @@
use std::io;
use std::path::Path;
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
/// `VfsBackend` that returns an error on every operation.
#[non_exhaustive]
pub struct NoopBackend;
impl NoopBackend {
pub fn new() -> Self {
Self
}
}
impl VfsBackend for NoopBackend {
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
crossbeam_channel::never()
}
fn watch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
}
}

View File

@@ -1,8 +1,8 @@
use std::path::{Path, PathBuf}; use std::io;
use std::path::Path;
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use std::{collections::HashSet, io};
use crossbeam_channel::Receiver; use crossbeam_channel::Receiver;
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
@@ -13,7 +13,6 @@ use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
pub struct StdBackend { pub struct StdBackend {
watcher: RecommendedWatcher, watcher: RecommendedWatcher,
watcher_receiver: Receiver<VfsEvent>, watcher_receiver: Receiver<VfsEvent>,
watches: HashSet<PathBuf>,
} }
impl StdBackend { impl StdBackend {
@@ -49,7 +48,6 @@ impl StdBackend {
Self { Self {
watcher, watcher,
watcher_receiver: rx, watcher_receiver: rx,
watches: HashSet::new(),
} }
} }
} }
@@ -63,10 +61,6 @@ impl VfsBackend for StdBackend {
fs_err::write(path, data) fs_err::write(path, data)
} }
fn exists(&mut self, path: &Path) -> io::Result<bool> {
std::fs::exists(path)
}
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect(); let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
let mut entries = entries?; let mut entries = entries?;
@@ -82,14 +76,6 @@ impl VfsBackend for StdBackend {
}) })
} }
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
fs_err::create_dir(path)
}
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
fs_err::create_dir_all(path)
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> { fn remove_file(&mut self, path: &Path) -> io::Result<()> {
fs_err::remove_file(path) fs_err::remove_file(path)
} }
@@ -106,37 +92,19 @@ impl VfsBackend for StdBackend {
}) })
} }
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
fs_err::canonicalize(path)
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
self.watcher_receiver.clone() self.watcher_receiver.clone()
} }
fn watch(&mut self, path: &Path) -> io::Result<()> { fn watch(&mut self, path: &Path) -> io::Result<()> {
if self.watches.contains(path)
|| path
.ancestors()
.any(|ancestor| self.watches.contains(ancestor))
{
Ok(())
} else {
self.watches.insert(path.to_path_buf());
self.watcher self.watcher
.watch(path, RecursiveMode::Recursive) .watch(path, RecursiveMode::NonRecursive)
.map_err(io::Error::other) .map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
}
} }
fn unwatch(&mut self, path: &Path) -> io::Result<()> { fn unwatch(&mut self, path: &Path) -> io::Result<()> {
self.watches.remove(path); self.watcher
self.watcher.unwatch(path).map_err(io::Error::other) .unwatch(path)
} .map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
}
impl Default for StdBackend {
fn default() -> Self {
Self::new()
} }
} }

View File

@@ -1,27 +0,0 @@
{
"name": "Rojo",
"tree": {
"$className": "Folder",
"Plugin": {
"$path": "plugin/src"
},
"Packages": {
"$path": "plugin/Packages",
"Log": {
"$path": "plugin/log"
},
"Http": {
"$path": "plugin/http"
},
"Fmt": {
"$path": "plugin/fmt"
},
"RbxDom": {
"$path": "plugin/rbx_dom_lua"
}
},
"Version": {
"$path": "plugin/Version.txt"
}
}
}

View File

@@ -1 +0,0 @@
7.7.0-rc.1

View File

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

View File

@@ -25,7 +25,7 @@
local function defaultTableDebug(buffer, input) local function defaultTableDebug(buffer, input)
buffer:writeRaw("{") buffer:writeRaw("{")
for key, value in input do for key, value in pairs(input) do
buffer:write("[{:?}] = {:?}", key, value) buffer:write("[{:?}] = {:?}", key, value)
if next(input, key) ~= nil then if next(input, key) ~= nil then
@@ -50,7 +50,7 @@ local function defaultTableDebugExtended(buffer, input)
buffer:writeLineRaw("{") buffer:writeLineRaw("{")
buffer:indent() buffer:indent()
for key, value in input do for key, value in pairs(input) do
buffer:writeLine("[{:?}] = {:#?},", key, value) buffer:writeLine("[{:?}] = {:#?},", key, value)
end end

View File

@@ -3,12 +3,12 @@ Error.__index = Error
Error.Kind = { Error.Kind = {
HttpNotEnabled = { HttpNotEnabled = {
message = "Rojo requires HTTP access, which is not enabled.\n" message = "Rojo requires HTTP access, which is not enabled.\n" ..
.. "Check your game settings, located in the 'Home' tab of Studio.", "Check your game settings, located in the 'Home' tab of Studio.",
}, },
ConnectFailed = { ConnectFailed = {
message = "Couldn't connect to the Rojo server.\n" message = "Couldn't connect to the Rojo server.\n" ..
.. "Make sure the server is running — use 'rojo serve' to run it!", "Make sure the server is running — use 'rojo serve' to run it!",
}, },
Timeout = { Timeout = {
message = "HTTP request timed out.", message = "HTTP request timed out.",
@@ -63,13 +63,4 @@ function Error.fromRobloxErrorString(message)
return Error.new(Error.Kind.Unknown, message) return Error.new(Error.Kind.Unknown, message)
end end
function Error.fromResponse(response)
local lower = (response.body or ""):lower()
if response.code == 408 or response.code == 504 or lower:find("timed? ?out") then
return Error.new(Error.Kind.Timeout)
end
return Error.new(Error.Kind.Unknown, string.format("%s: %s", tostring(response.code), tostring(response.body)))
end
return Error return Error

View File

@@ -1,7 +1,5 @@
local HttpService = game:GetService("HttpService") local HttpService = game:GetService("HttpService")
local msgpack = require(script.Parent.Parent.msgpack)
local stringTemplate = [[ local stringTemplate = [[
Http.Response { Http.Response {
code: %d code: %d
@@ -33,8 +31,4 @@ function Response:json()
return HttpService:JSONDecode(self.body) return HttpService:JSONDecode(self.body)
end end
function Response:msgpack()
return msgpack.decode(self.body)
end
return Response return Response

View File

@@ -1,8 +1,7 @@
local HttpService = game:GetService("HttpService") local HttpService = game:GetService("HttpService")
local Log = require(script.Parent.Log)
local msgpack = require(script.Parent.msgpack)
local Promise = require(script.Parent.Promise) local Promise = require(script.Parent.Promise)
local Log = require(script.Parent.Log)
local HttpError = require(script.Error) local HttpError = require(script.Error)
local HttpResponse = require(script.Response) local HttpResponse = require(script.Response)
@@ -31,13 +30,8 @@ local function performRequest(requestParams)
end) end)
if success then if success then
Log.trace("Request {} success, response {:#?}", requestId, response) Log.trace("Request {} success, status code {}", requestId, response.StatusCode)
local httpResponse = HttpResponse.fromRobloxResponse(response) resolve(HttpResponse.fromRobloxResponse(response))
if httpResponse:isSuccess() then
resolve(httpResponse)
else
reject(HttpError.fromResponse(httpResponse))
end
else else
Log.trace("Request {} failure: {:?}", requestId, response) Log.trace("Request {} failure: {:?}", requestId, response)
reject(HttpError.fromRobloxErrorString(response)) reject(HttpError.fromRobloxErrorString(response))
@@ -69,12 +63,4 @@ function Http.jsonDecode(source)
return HttpService:JSONDecode(source) return HttpService:JSONDecode(source)
end end
function Http.msgpackEncode(object)
return msgpack.encode(object)
end
function Http.msgpackDecode(source)
return msgpack.decode(source)
end
return Http return Http

1
plugin/modules/roact Submodule

Submodule plugin/modules/roact added at f7d2f1ce1d

1
plugin/modules/t Submodule

Submodule plugin/modules/t added at f643b50682

1
plugin/modules/testez Submodule

Submodule plugin/modules/testez added at 6e9157db3c

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

@@ -1,622 +0,0 @@
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 ALL_AXES = { "X", "Y", "Z" }
local ALL_FACES = { "Right", "Top", "Back", "Left", "Bottom", "Front" }
local EncodedValue = {}
local types
types = {
Attributes = {
fromPod = function(pod)
local output = {}
for key, value in pairs(pod) do
local ok, result = EncodedValue.decode(value)
if ok then
output[key] = result
else
local warning = ("Could not decode attribute value of type %q: %s"):format(
typeof(value),
tostring(result)
)
warn(warning)
end
end
return output
end,
toPod = function(roblox)
local output = {}
for key, value in pairs(roblox) do
local ok, result = EncodedValue.encodeNaive(value)
if ok then
output[key] = result
else
local warning = ("Could not encode attribute value of type %q: %s"):format(
typeof(value),
tostring(result)
)
warn(warning)
end
end
return output
end,
},
Axes = {
fromPod = function(pod)
local axes = {}
for index, axisName in ipairs(pod) do
axes[index] = Enum.Axis[axisName]
end
return Axes.new(unpack(axes))
end,
toPod = function(roblox)
local json = {}
for _, axis in ipairs(ALL_AXES) do
if roblox[axis] then
table.insert(json, axis)
end
end
return json
end,
},
BinaryString = {
fromPod = base64.decode,
toPod = base64.encode,
},
Bool = {
fromPod = identity,
toPod = identity,
},
BrickColor = {
fromPod = function(pod)
return BrickColor.new(pod)
end,
toPod = function(roblox)
return roblox.Number
end,
},
CFrame = {
fromPod = function(pod)
local pos = pod.position
local orient = pod.orientation
--stylua: ignore
return CFrame.new(
pos[1], pos[2], pos[3],
orient[1][1], orient[1][2], orient[1][3],
orient[2][1], orient[2][2], orient[2][3],
orient[3][1], orient[3][2], orient[3][3]
)
end,
toPod = function(roblox)
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = roblox:GetComponents()
return {
position = { x, y, z },
orientation = {
{ r00, r01, r02 },
{ r10, r11, r12 },
{ r20, r21, r22 },
},
}
end,
},
Color3 = {
fromPod = unpackDecoder(Color3.new),
toPod = function(roblox)
return { roblox.r, roblox.g, roblox.b }
end,
},
Color3uint8 = {
fromPod = unpackDecoder(Color3.fromRGB),
toPod = function(roblox)
return {
math.round(roblox.R * 255),
math.round(roblox.G * 255),
math.round(roblox.B * 255),
}
end,
},
ColorSequence = {
fromPod = function(pod)
local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = ColorSequenceKeypoint.new(keypoint.time, types.Color3.fromPod(keypoint.color))
end
return ColorSequence.new(keypoints)
end,
toPod = function(roblox)
local keypoints = {}
for index, keypoint in ipairs(roblox.Keypoints) do
keypoints[index] = {
time = keypoint.Time,
color = types.Color3.toPod(keypoint.Value),
}
end
return {
keypoints = keypoints,
}
end,
},
Content = {
fromPod = function(pod): Content
if type(pod) == "string" then
if pod == "None" then
return Content.none
else
error(`unexpected Content value '{pod}'`)
end
else
local ty, value = next(pod)
if ty == "Uri" then
return Content.fromUri(value)
elseif ty == "Object" then
error("Object deserializing is not currently implemented")
else
error(`Unknown Content type '{ty}' (could not deserialize)`)
end
end
end,
toPod = function(roblox: Content)
if roblox.SourceType == Enum.ContentSourceType.None then
return "None"
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
return { Uri = roblox.Uri }
elseif roblox.SourceType == Enum.ContentSourceType.Object then
error("Object serializing is not currently implemented")
else
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
end
end,
},
ContentId = {
fromPod = identity,
toPod = identity,
},
Enum = {
fromPod = identity,
toPod = function(roblox)
-- FIXME: More robust handling of enums
if typeof(roblox) == "number" then
return roblox
else
return roblox.Value
end
end,
},
EnumItem = {
fromPod = function(pod)
return Enum[pod.type]:FromValue(pod.value)
end,
toPod = function(roblox)
return {
type = tostring(roblox.EnumType),
value = roblox.Value,
}
end,
},
Faces = {
fromPod = function(pod)
local faces = {}
for index, faceName in ipairs(pod) do
faces[index] = Enum.NormalId[faceName]
end
return Faces.new(unpack(faces))
end,
toPod = function(roblox)
local pod = {}
for _, face in ipairs(ALL_FACES) do
if roblox[face] then
table.insert(pod, face)
end
end
return pod
end,
},
Float32 = {
fromPod = identity,
toPod = serializeFloat,
},
Float64 = {
fromPod = identity,
toPod = serializeFloat,
},
Font = {
fromPod = function(pod)
return Font.new(
pod.family,
if pod.weight ~= nil then Enum.FontWeight[pod.weight] else nil,
if pod.style ~= nil then Enum.FontStyle[pod.style] else nil
)
end,
toPod = function(roblox)
return {
family = roblox.Family,
weight = roblox.Weight.Name,
style = roblox.Style.Name,
}
end,
},
Int32 = {
fromPod = identity,
toPod = identity,
},
Int64 = {
fromPod = identity,
toPod = identity,
},
MaterialColors = {
fromPod = function(pod: { [string]: { number } })
local real = {}
for name, color in pod do
real[Enum.Material[name]] = Color3.fromRGB(color[1], color[2], color[3])
end
return real
end,
toPod = function(roblox: { [Enum.Material]: Color3 })
local pod = {}
for material, color in roblox do
pod[material.Name] = {
math.round(math.clamp(color.R, 0, 1) * 255),
math.round(math.clamp(color.G, 0, 1) * 255),
math.round(math.clamp(color.B, 0, 1) * 255),
}
end
return pod
end,
},
NumberRange = {
fromPod = unpackDecoder(NumberRange.new),
toPod = function(roblox)
return { roblox.Min, roblox.Max }
end,
},
NumberSequence = {
fromPod = function(pod)
local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do
-- TODO: Add a test for NaN or Infinity values and envelopes
-- Right now it isn't possible because it'd fail the roundtrip.
-- It's more important that it works right now, though.
local value = keypoint.value or 0
local envelope = keypoint.envelope or 0
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
end
return NumberSequence.new(keypoints)
end,
toPod = function(roblox)
local keypoints = {}
for index, keypoint in ipairs(roblox.Keypoints) do
keypoints[index] = {
time = keypoint.Time,
value = keypoint.Value,
envelope = keypoint.Envelope,
}
end
return {
keypoints = keypoints,
}
end,
},
PhysicalProperties = {
fromPod = function(pod)
if pod == "Default" then
return nil
else
-- Passing `nil` instead of not passing anything gives
-- different results, so we have to branch here.
if pod.acousticAbsorption then
return (PhysicalProperties.new :: any)(
pod.density,
pod.friction,
pod.elasticity,
pod.frictionWeight,
pod.elasticityWeight,
pod.acousticAbsorption
)
else
return PhysicalProperties.new(
pod.density,
pod.friction,
pod.elasticity,
pod.frictionWeight,
pod.elasticityWeight
)
end
end
end,
toPod = function(roblox)
if roblox == nil then
return "Default"
else
return {
density = roblox.Density,
friction = roblox.Friction,
elasticity = roblox.Elasticity,
frictionWeight = roblox.FrictionWeight,
elasticityWeight = roblox.ElasticityWeight,
acousticAbsorption = roblox.AcousticAbsorption,
}
end
end,
},
Ray = {
fromPod = function(pod)
return Ray.new(types.Vector3.fromPod(pod.origin), types.Vector3.fromPod(pod.direction))
end,
toPod = function(roblox)
return {
origin = types.Vector3.toPod(roblox.Origin),
direction = types.Vector3.toPod(roblox.Direction),
}
end,
},
Rect = {
fromPod = function(pod)
return Rect.new(types.Vector2.fromPod(pod[1]), types.Vector2.fromPod(pod[2]))
end,
toPod = function(roblox)
return {
types.Vector2.toPod(roblox.Min),
types.Vector2.toPod(roblox.Max),
}
end,
},
Ref = {
fromPod = function(_)
error("Ref cannot be decoded on its own")
end,
toPod = function(_)
error("Ref can not be encoded on its own")
end,
},
Region3 = {
fromPod = function(_)
error("Region3 is not implemented")
end,
toPod = function(_)
error("Region3 is not implemented")
end,
},
Region3int16 = {
fromPod = function(pod)
return Region3int16.new(types.Vector3int16.fromPod(pod[1]), types.Vector3int16.fromPod(pod[2]))
end,
toPod = function(roblox)
return {
types.Vector3int16.toPod(roblox.Min),
types.Vector3int16.toPod(roblox.Max),
}
end,
},
SharedString = {
fromPod = function(_pod)
error("SharedString is not supported")
end,
toPod = function(_roblox)
error("SharedString is not supported")
end,
},
String = {
fromPod = identity,
toPod = identity,
},
UDim = {
fromPod = unpackDecoder(UDim.new),
toPod = function(roblox)
return { roblox.Scale, roblox.Offset }
end,
},
UDim2 = {
fromPod = function(pod)
return UDim2.new(types.UDim.fromPod(pod[1]), types.UDim.fromPod(pod[2]))
end,
toPod = function(roblox)
return {
types.UDim.toPod(roblox.X),
types.UDim.toPod(roblox.Y),
}
end,
},
Tags = {
fromPod = identity,
toPod = identity,
},
Vector2 = {
fromPod = unpackDecoder(Vector2.new),
toPod = function(roblox)
return {
serializeFloat(roblox.X),
serializeFloat(roblox.Y),
}
end,
},
Vector2int16 = {
fromPod = unpackDecoder(Vector2int16.new),
toPod = function(roblox)
return { roblox.X, roblox.Y }
end,
},
Vector3 = {
fromPod = unpackDecoder(Vector3.new),
toPod = function(roblox)
return {
serializeFloat(roblox.X),
serializeFloat(roblox.Y),
serializeFloat(roblox.Z),
}
end,
},
Vector3int16 = {
fromPod = unpackDecoder(Vector3int16.new),
toPod = function(roblox)
return { roblox.X, roblox.Y, roblox.Z }
end,
},
}
types.OptionalCFrame = {
fromPod = function(pod)
if pod == nil then
return nil
else
return types.CFrame.fromPod(pod)
end
end,
toPod = function(roblox)
if roblox == nil then
return nil
else
return types.CFrame.toPod(roblox)
end
end,
}
function EncodedValue.decode(encodedValue)
local ty, value = next(encodedValue)
if ty == nil then
-- If the encoded pair is empty, assume it is an unoccupied optional value
return true, nil
end
local typeImpl = types[ty]
if typeImpl == nil then
return false, "Couldn't decode value " .. tostring(ty)
end
return true, typeImpl.fromPod(value)
end
function EncodedValue.encode(rbxValue, propertyType)
assert(propertyType ~= nil, "Property type descriptor is required")
local typeImpl = types[propertyType]
if typeImpl == nil then
return false, ("Missing encoder for property type %q"):format(propertyType)
end
return true, {
[propertyType] = typeImpl.toPod(rbxValue),
}
end
local propertyTypeRenames = {
number = "Float64",
boolean = "Bool",
string = "String",
}
function EncodedValue.encodeNaive(rbxValue)
local propertyType = typeof(rbxValue)
if propertyTypeRenames[propertyType] ~= nil then
propertyType = propertyTypeRenames[propertyType]
end
return EncodedValue.encode(rbxValue, propertyType)
end
return EncodedValue

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

@@ -1,583 +0,0 @@
{
"Attributes": {
"value": {
"Attributes": {
"TestBool": {
"Bool": true
},
"TestBrickColor": {
"BrickColor": 24
},
"TestColor3": {
"Color3": [
1.0,
0.5,
0.0
]
},
"TestEnumItem": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"TestNumber": {
"Float64": 1337.0
},
"TestRect": {
"Rect": [
[
1.0,
2.0
],
[
3.0,
4.0
]
]
},
"TestString": {
"String": "Test"
},
"TestUDim": {
"UDim": [
1.0,
2
]
},
"TestUDim2": {
"UDim2": [
[
1.0,
2
],
[
3.0,
4
]
]
},
"TestVector2": {
"Vector2": [
1.0,
2.0
]
},
"TestVector3": {
"Vector3": [
1.0,
2.0,
3.0
]
}
}
},
"ty": "Attributes"
},
"Axes": {
"value": {
"Axes": [
"X",
"Y",
"Z"
]
},
"ty": "Axes"
},
"BinaryString": {
"value": {
"BinaryString": "SGVsbG8h"
},
"ty": "BinaryString"
},
"Bool": {
"value": {
"Bool": true
},
"ty": "Bool"
},
"BrickColor": {
"value": {
"BrickColor": 1004
},
"ty": "BrickColor"
},
"CFrame": {
"value": {
"CFrame": {
"position": [
1.0,
2.0,
3.0
],
"orientation": [
[
4.0,
5.0,
6.0
],
[
7.0,
8.0,
9.0
],
[
10.0,
11.0,
12.0
]
]
}
},
"ty": "CFrame"
},
"Color3": {
"value": {
"Color3": [
1.0,
2.0,
3.0
]
},
"ty": "Color3"
},
"Color3uint8": {
"value": {
"Color3uint8": [
0,
128,
255
]
},
"ty": "Color3uint8"
},
"ColorSequence": {
"value": {
"ColorSequence": {
"keypoints": [
{
"time": 0.0,
"color": [
1.0,
1.0,
0.5
]
},
{
"time": 1.0,
"color": [
0.0,
0.0,
0.0
]
}
]
}
},
"ty": "ColorSequence"
},
"ContentId": {
"value": {
"ContentId": "rbxassetid://12345"
},
"ty": "ContentId"
},
"Content_None": {
"value": {
"Content": "None"
},
"ty": "Content"
},
"Content_Uri": {
"value": {
"Content": {
"Uri": "rbxasset://abc/123.rojo"
}
},
"ty": "Content"
},
"Enum": {
"value": {
"Enum": 1234
},
"ty": "Enum"
},
"EnumItem": {
"value": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"ty": "EnumItem"
},
"Faces": {
"value": {
"Faces": [
"Right",
"Top",
"Back",
"Left",
"Bottom",
"Front"
]
},
"ty": "Faces"
},
"Float32": {
"value": {
"Float32": 15.0
},
"ty": "Float32"
},
"Float64": {
"value": {
"Float64": 15123.0
},
"ty": "Float64"
},
"Font": {
"value": {
"Font": {
"family": "rbxasset://fonts/families/SourceSansPro.json",
"weight": "Regular",
"style": "Normal",
"cachedFaceId": null
}
},
"ty": "Font"
},
"Int32": {
"value": {
"Int32": 6014
},
"ty": "Int32"
},
"Int64": {
"value": {
"Int64": 23491023
},
"ty": "Int64"
},
"MaterialColors": {
"value": {
"MaterialColors": {
"Grass": [
106,
127,
63
],
"Slate": [
63,
127,
107
],
"Concrete": [
127,
102,
63
],
"Brick": [
138,
86,
62
],
"Sand": [
143,
126,
95
],
"WoodPlanks": [
139,
109,
79
],
"Rock": [
102,
108,
111
],
"Glacier": [
101,
176,
234
],
"Snow": [
195,
199,
218
],
"Sandstone": [
137,
90,
71
],
"Mud": [
58,
46,
36
],
"Basalt": [
30,
30,
37
],
"Ground": [
102,
92,
59
],
"CrackedLava": [
232,
156,
74
],
"Asphalt": [
115,
123,
107
],
"Cobblestone": [
132,
123,
90
],
"Ice": [
129,
194,
224
],
"LeafyGrass": [
115,
132,
74
],
"Salt": [
198,
189,
181
],
"Limestone": [
206,
173,
148
],
"Pavement": [
148,
148,
140
]
}
},
"ty": "MaterialColors"
},
"NumberRange": {
"value": {
"NumberRange": [
-36.0,
94.0
]
},
"ty": "NumberRange"
},
"NumberSequence": {
"value": {
"NumberSequence": {
"keypoints": [
{
"time": 0.0,
"value": 5.0,
"envelope": 2.0
},
{
"time": 1.0,
"value": 22.0,
"envelope": 0.0
}
]
}
},
"ty": "NumberSequence"
},
"OptionalCFrame-None": {
"value": {
"OptionalCFrame": null
},
"ty": "OptionalCFrame"
},
"OptionalCFrame-Some": {
"value": {
"OptionalCFrame": {
"position": [
0.0,
0.0,
0.0
],
"orientation": [
[
1.0,
0.0,
0.0
],
[
0.0,
1.0,
0.0
],
[
0.0,
0.0,
1.0
]
]
}
},
"ty": "OptionalCFrame"
},
"PhysicalProperties-Custom": {
"value": {
"PhysicalProperties": {
"density": 0.5,
"friction": 1.0,
"elasticity": 0.0,
"frictionWeight": 50.0,
"elasticityWeight": 25.0,
"acousticAbsorption": 0.15625
}
},
"ty": "PhysicalProperties"
},
"PhysicalProperties-Default": {
"value": {
"PhysicalProperties": "Default"
},
"ty": "PhysicalProperties"
},
"Ray": {
"value": {
"Ray": {
"origin": [
1.0,
2.0,
3.0
],
"direction": [
4.0,
5.0,
6.0
]
}
},
"ty": "Ray"
},
"Rect": {
"value": {
"Rect": [
[
0.0,
5.0
],
[
10.0,
15.0
]
]
},
"ty": "Rect"
},
"Region3int16": {
"value": {
"Region3int16": [
[
-10,
-5,
0
],
[
5,
10,
15
]
]
},
"ty": "Region3int16"
},
"String": {
"value": {
"String": "Hello, world!"
},
"ty": "String"
},
"Tags": {
"value": {
"Tags": [
"foo",
"con'fusion?!",
"bar"
]
},
"ty": "Tags"
},
"UDim": {
"value": {
"UDim": [
1.0,
32
]
},
"ty": "UDim"
},
"UDim2": {
"value": {
"UDim2": [
[
-1.0,
100
],
[
1.0,
-100
]
]
},
"ty": "UDim2"
},
"Vector2": {
"value": {
"Vector2": [
-50.0,
50.0
]
},
"ty": "Vector2"
},
"Vector2int16": {
"value": {
"Vector2int16": [
-300,
300
]
},
"ty": "Vector2int16"
},
"Vector3": {
"value": {
"Vector3": [
-300.0,
0.0,
1500.0
]
},
"ty": "Vector3"
},
"Vector3int16": {
"value": {
"Vector3int16": [
60,
37,
-450
]
},
"ty": "Vector3int16"
}
}

View File

@@ -1,10 +0,0 @@
local EncodingService = game:GetService("EncodingService")
return {
decode = function(input: string)
return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input)))
end,
encode = function(input: string)
return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input)))
end,
}

View File

@@ -1,237 +0,0 @@
local CollectionService = game:GetService("CollectionService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local Error = require(script.Parent.Error)
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
local TERRAIN_MATERIAL_COLORS = {
Enum.Material.Grass,
Enum.Material.Slate,
Enum.Material.Concrete,
Enum.Material.Brick,
Enum.Material.Sand,
Enum.Material.WoodPlanks,
Enum.Material.Rock,
Enum.Material.Glacier,
Enum.Material.Snow,
Enum.Material.Sandstone,
Enum.Material.Mud,
Enum.Material.Basalt,
Enum.Material.Ground,
Enum.Material.CrackedLava,
Enum.Material.Asphalt,
Enum.Material.Cobblestone,
Enum.Material.Ice,
Enum.Material.LeafyGrass,
Enum.Material.Salt,
Enum.Material.Limestone,
Enum.Material.Pavement,
}
local function isAttributeNameValid(attributeName)
-- For SetAttribute to succeed, the attribute name must be less than or
-- equal to 100 characters...
return #attributeName <= 100
-- ...and must only contain alphanumeric characters, periods, hyphens,
-- underscores, or forward slashes.
and attributeName:match("[^%w%.%-_/]") == nil
end
local function isAttributeNameReserved(attributeName)
-- For SetAttribute to succeed, attribute names must not use the RBX
-- prefix, which is reserved by Roblox.
return attributeName:sub(1, 3) == "RBX"
end
-- Defines how to read and write properties that aren't directly scriptable.
--
-- The reflection database refers to these as having scriptability = "Custom"
return {
Instance = {
Attributes = {
read = function(instance)
return true, instance:GetAttributes()
end,
write = function(instance, _, value)
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
local existing = instance:GetAttributes()
local didAllWritesSucceed = true
for attributeName, attributeValue in pairs(value) do
if isAttributeNameReserved(attributeName) then
-- If the attribute name is reserved, then we don't
-- really care about reporting any failures about
-- it.
continue
end
if not isAttributeNameValid(attributeName) then
didAllWritesSucceed = false
continue
end
instance:SetAttribute(attributeName, attributeValue)
end
for existingAttributeName in pairs(existing) do
if isAttributeNameReserved(existingAttributeName) then
continue
end
if not isAttributeNameValid(existingAttributeName) then
didAllWritesSucceed = false
continue
end
if value[existingAttributeName] == nil then
instance:SetAttribute(existingAttributeName, nil)
end
end
return didAllWritesSucceed
end,
},
Tags = {
read = function(instance)
return true, CollectionService:GetTags(instance)
end,
write = function(instance, _, value)
local existingTags = CollectionService:GetTags(instance)
local unseenTags = {}
for _, tag in ipairs(existingTags) do
unseenTags[tag] = true
end
for _, tag in ipairs(value) 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, _)
return true, instance:GetContents()
end,
write = function(instance, _, value)
instance:SetContents(value)
return true
end,
},
},
Model = {
Scale = {
read = function(instance, _, _)
return true, instance:GetScale()
end,
write = function(instance, _, value)
return true, instance:ScaleTo(value)
end,
},
WorldPivotData = {
read = function(instance)
return true, instance.WorldPivot
end,
write = function(instance, _, value)
if value == nil then
return true, nil
else
instance.WorldPivot = value
return true
end
end,
},
},
Terrain = {
MaterialColors = {
read = function(instance: Terrain)
-- There's no way to get a list of every color, so we have to
-- make one.
local colors = {}
for _, material in TERRAIN_MATERIAL_COLORS do
colors[material] = instance:GetMaterialColor(material)
end
return true, colors
end,
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
for material, color in value do
instance:SetMaterialColor(material, color)
end
return true
end,
},
},
Script = {
Source = {
read = function(instance: Script)
return true, ScriptEditorService:GetEditorSource(instance)
end,
write = function(instance: Script, _, value: string)
task.spawn(function()
ScriptEditorService:UpdateSourceAsync(instance, function()
return value
end)
end)
return true
end,
},
},
ModuleScript = {
Source = {
read = function(instance: ModuleScript)
return true, ScriptEditorService:GetEditorSource(instance)
end,
write = function(instance: ModuleScript, _, value: string)
task.spawn(function()
ScriptEditorService:UpdateSourceAsync(instance, function()
return value
end)
end)
return true
end,
},
},
StyleRule = {
PropertiesSerialize = {
read = function(instance: StyleRule)
return true, instance:GetProperties()
end,
write = function(instance: StyleRule, _, value: { [any]: any })
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
local existing = instance:GetProperties()
for itemName, itemValue in pairs(value) do
instance:SetProperty(itemName, itemValue)
end
for existingItemName in pairs(existing) do
if value[existingItemName] == nil then
instance:SetProperty(existingItemName, nil)
end
end
return true
end,
},
},
}

File diff suppressed because it is too large Load Diff

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