Compare commits

..

1 Commits

Author SHA1 Message Date
Lucien Greathouse
b96a236333 Refactor project file into its own crate 2022-05-26 05:00:57 -04:00
419 changed files with 7995 additions and 67368 deletions

View File

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

View File

@@ -23,7 +23,4 @@ insert_final_newline = true
insert_final_newline = true insert_final_newline = true
[*.lua] [*.lua]
indent_style = tab
[*.luau]
indent_style = tab indent_style = tab

View File

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

1
.gitattributes vendored
View File

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

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
patreon: lpghatguy

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

@@ -12,28 +12,23 @@ on:
jobs: jobs:
build: build:
name: Build and Test 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.55.0]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
- name: Restore Rust Cache
uses: actions/cache/restore@v4
with: with:
path: | toolchain: ${{ matrix.rust_version }}
~/.cargo/registry override: true
~/.cargo/git profile: minimal
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
@@ -41,93 +36,24 @@ jobs:
- name: Test - name: Test
run: cargo test --locked --verbose run: cargo test --locked --verbose
- name: Save Rust Cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.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: lint:
name: Rustfmt, Clippy, Stylua, & Selene name: Rustfmt and Clippy
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
with: with:
toolchain: stable
override: true
components: rustfmt, clippy 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 - name: Rustfmt
run: cargo fmt -- --check run: cargo fmt -- --check
- name: Clippy - name: Clippy
run: cargo 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

@@ -8,39 +8,49 @@ jobs:
create-release: create-release:
name: Create Release name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
- uses: actions/checkout@v4
- name: Create Release - name: Create Release
id: create_release
uses: actions/create-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | with:
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }} tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: false
build-plugin: build-plugin:
needs: ["create-release"] needs: ["create-release"]
name: Build Roblox Studio Plugin name: Build Roblox Studio Plugin
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Setup Rokit - name: Setup Foreman
uses: CompeyDev/setup-rokit@v0.1.2 uses: Roblox/setup-foreman@v1
with: with:
version: 'v1.1.0' token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Plugin - name: Build Plugin
run: rojo build plugin.project.json --output Rojo.rbxm run: rojo build plugin --output Rojo.rbxm
- name: Upload Plugin to Release - name: Upload Plugin to Release
uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | with:
gh release upload ${{ github.ref_name }} Rojo.rbxm upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: Rojo.rbxm
asset_name: Rojo.rbxm
asset_content_type: application/octet-stream
- name: Upload Plugin to Artifacts - name: Upload Plugin to Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: Rojo.rbxm name: Rojo.rbxm
path: Rojo.rbxm path: Rojo.rbxm
@@ -51,31 +61,25 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
# https://doc.rust-lang.org/rustc/platform-support.html # https://doc.rust-lang.org/rustc/platform-support.html
#
# FIXME: After the Rojo VS Code extension updates, add architecture
# names to each of these releases. We'll rename win64 to windows and add
# -x86_64 to each release.
include: include:
- host: linux - host: linux
os: ubuntu-22.04 os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
label: linux-x86_64 label: linux
- host: linux
os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
label: linux-aarch64
- host: windows - host: windows
os: windows-latest os: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
label: windows-x86_64 label: win64
- host: windows
os: windows-11-arm
target: aarch64-pc-windows-msvc
label: windows-aarch64
- host: macos - host: macos
os: macos-latest os: macos-latest
target: x86_64-apple-darwin target: x86_64-apple-darwin
label: macos-x86_64 label: macos
- host: macos - host: macos
os: macos-latest os: macos-latest
@@ -87,64 +91,63 @@ jobs:
env: env:
BIN: rojo BIN: rojo
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Get Version from Tag
uses: dtolnay/rust-toolchain@stable shell: bash
with: # https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
targets: ${{ matrix.target }} run: |
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
echo "Version is: ${{ env.PROJECT_VERSION }}"
- name: Restore Rust Cache - name: Install Rust
uses: actions/cache/restore@v4 uses: actions-rs/toolchain@v1
with: with:
path: | toolchain: stable
~/.cargo/registry target: ${{ matrix.target }}
~/.cargo/git override: true
target profile: minimal
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build Release - name: Build Release
run: cargo build --release --locked --verbose --target ${{ matrix.target }} run: cargo build --release --locked --verbose
- 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: env:
TAG_NAME: ${{ github.ref_name }} # Build into a known directory so we can find our build artifact more
run: | # easily.
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV" CARGO_TARGET_DIR: output
- name: Create Archive and Upload to Release # On platforms that use OpenSSL, ensure it is statically linked to
# make binaries more portable.
OPENSSL_STATIC: 1
- name: Create Release Archive
shell: bash shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
mkdir staging mkdir staging
if [ "${{ matrix.host }}" = "windows" ]; then if [ "${{ matrix.host }}" = "windows" ]; then
cp "target/${{ matrix.target }}/release/$BIN.exe" staging/ cp "output/release/$BIN.exe" staging/
cd staging cd staging
7z a ../$ARTIFACT_NAME * 7z a ../release.zip *
else else
cp "target/${{ matrix.target }}/release/$BIN" staging/ cp "output/release/$BIN" staging/
cd staging cd staging
zip ../$ARTIFACT_NAME * zip ../release.zip *
fi fi
gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME - name: Upload Archive to Release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: release.zip
asset_name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
asset_content_type: application/octet-stream
- name: Upload Archive to Artifacts - name: Upload Archive to Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
path: ${{ env.ARTIFACT_NAME }} name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
name: ${{ env.ARTIFACT_NAME }} path: release.zip

9
.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,6 +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

31
.gitmodules vendored
View File

@@ -1,18 +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

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

@@ -15,29 +15,12 @@ You'll want these tools to work on Rojo:
* Latest stable Rust compiler * Latest stable Rust compiler
* 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.

1950
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,19 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.6.1" version = "7.1.1"
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" build = "build.rs"
exclude = ["/test-projects/**"] exclude = [
"/test-projects/**",
]
[profile.dev] [profile.dev]
panic = "abort" panic = "abort"
@@ -30,10 +27,6 @@ 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 = ["crates/*"]
@@ -46,7 +39,8 @@ name = "build"
harness = false harness = false
[dependencies] [dependencies]
memofs = { version = "0.3.0", path = "crates/memofs" } rojo-project = { path = "crates/rojo-project" }
memofs = { version = "0.2.0", path = "crates/memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously # These dependencies can be uncommented when working on rbx-dom simultaneously
# rbx_binary = { path = "../rbx-dom/rbx_binary" } # rbx_binary = { path = "../rbx-dom/rbx_binary" }
@@ -55,68 +49,58 @@ memofs = { version = "0.3.0", path = "crates/memofs" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" } # rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" } # rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "2.0.0" rbx_binary = "0.6.4"
rbx_dom_weak = "4.0.0" rbx_dom_weak = "2.3.0"
rbx_reflection = "6.0.0" rbx_reflection = "4.2.0"
rbx_reflection_database = "2.0.1" rbx_reflection_database = "0.2.2"
rbx_xml = "2.0.0" rbx_xml = "0.12.3"
anyhow = "1.0.80" anyhow = "1.0.44"
backtrace = "0.3.69" backtrace = "0.3.61"
bincode = "1.3.3" bincode = "1.3.3"
crossbeam-channel = "0.5.12" crossbeam-channel = "0.5.1"
csv = "1.3.0" csv = "1.1.6"
env_logger = "0.9.3" env_logger = "0.9.0"
fs-err = "2.11.0" fs-err = "2.6.0"
futures = "0.3.30" futures = "0.3.17"
globset = "0.4.14" globset = "0.4.8"
humantime = "2.1.0" humantime = "2.1.0"
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] } hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
jod-thread = "0.1.2" jod-thread = "0.1.2"
log = "0.4.21" log = "0.4.14"
num_cpus = "1.16.0" maplit = "1.0.2"
opener = "0.5.2" notify = "4.0.17"
rayon = "1.9.0" opener = "0.5.0"
reqwest = { version = "0.11.24", default-features = false, features = [ reqwest = { version = "0.11.10", features = ["blocking", "json"] }
"blocking",
"json",
"rustls-tls",
] }
ritz = "0.1.0" ritz = "0.1.0"
roblox_install = "1.0.0" roblox_install = "1.0.0"
serde = { version = "1.0.197", features = ["derive", "rc"] } serde = { version = "1.0.130", features = ["derive", "rc"] }
serde_json = "1.0.145" serde_json = "1.0.68"
jsonc-parser = { version = "0.27.0", features = ["serde"] } termcolor = "1.1.2"
toml = "0.5.11" thiserror = "1.0.30"
termcolor = "1.4.1" tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
thiserror = "1.0.57" uuid = { version = "1.0.0", features = ["v4", "serde"] }
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] } clap = { version = "3.1.18", features = ["derive"] }
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"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.10.1" winreg = "0.10.1"
[build-dependencies] [build-dependencies]
memofs = { version = "0.3.0", path = "crates/memofs" } memofs = { version = "0.2.0", path = "crates/memofs" }
embed-resource = "1.8.0" embed-resource = "1.6.4"
anyhow = "1.0.80" anyhow = "1.0.44"
bincode = "1.3.3" bincode = "1.3.3"
fs-err = "2.11.0" fs-err = "2.6.0"
maplit = "1.0.2" maplit = "1.0.2"
semver = "1.0.22"
[dev-dependencies] [dev-dependencies]
rojo-insta-ext = { path = "crates/rojo-insta-ext" } rojo-insta-ext = { path = "crates/rojo-insta-ext" }
criterion = "0.3.6" criterion = "0.3.5"
insta = { version = "1.36.1", features = ["redactions", "yaml"] } insta = { version = "1.8.0", features = ["redactions"] }
paste = "1.0.14" paste = "1.0.5"
pretty_assertions = "1.4.0" pretty_assertions = "1.2.1"
serde_yaml = "0.8.26" serde_yaml = "0.8.21"
tempfile = "3.10.1" tempfile = "3.2.0"
walkdir = "2.5.0" walkdir = "2.3.2"

View File

@@ -1,5 +1,5 @@
<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>
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome! Pull requests are welcome!
Rojo supports Rust 1.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.46.0 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.

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

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

View File

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

View File

@@ -3,6 +3,4 @@
# 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: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

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,
}; };

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,13 +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; continue;
} }
@@ -45,39 +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.rc");
println!("cargo:rerun-if-changed=build/windows/rojo.manifest"); println!("cargo:rerun-if-changed=build/windows/rojo.manifest");

View File

@@ -2,13 +2,6 @@
## Unreleased Changes ## Unreleased Changes
## 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) ## 0.2.0 (2021-08-23)
* Updated to `crossbeam-channel` 0.5.1. * Updated to `crossbeam-channel` 0.5.1.
@@ -22,4 +15,4 @@
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate. * Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
## 0.1.0 (2020-03-10) ## 0.1.0 (2020-03-10)
* Initial release * 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.0" version = "0.2.0"
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,7 +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.5.1"
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"] }

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>,
@@ -228,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(
"path {} was a directory, but must be a file", io::ErrorKind::Other,
path.display() format!(
))) "path {} was a directory, but must be a file",
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(
"path {} was a file, but must be a directory", io::ErrorKind::Other,
path.display() format!(
))) "path {} was a file, but must be a directory",
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;
@@ -155,24 +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 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();
@@ -212,8 +194,11 @@ impl VfsInner {
} }
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(())
@@ -276,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].
@@ -473,23 +431,3 @@ impl VfsLock<'_> {
self.inner.commit_event(event) self.inner.commit_event(event)
} }
} }
#[cfg(test)]
mod test {
use crate::{InMemoryFs, Vfs, VfsSnapshot};
/// 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"
);
}
}

View File

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

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(),
} }
} }
} }
@@ -99,28 +97,14 @@ impl VfsBackend for StdBackend {
} }
fn watch(&mut self, path: &Path) -> io::Result<()> { fn watch(&mut self, path: &Path) -> io::Result<()> {
if self.watches.contains(path) self.watcher
|| path .watch(path, RecursiveMode::NonRecursive)
.ancestors() .map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
.any(|ancestor| self.watches.contains(ancestor))
{
Ok(())
} else {
self.watches.insert(path.to_path_buf());
self.watcher
.watch(path, RecursiveMode::Recursive)
.map_err(io::Error::other)
}
} }
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

@@ -6,5 +6,5 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
serde = "1.0.197" serde = "1.0.99"
serde_yaml = "0.8.26" serde_yaml = "0.8.9"

View File

@@ -5,13 +5,19 @@ use serde::Serialize;
/// Enables redacting any value that serializes as a string. /// Enables redacting any value that serializes as a string.
/// ///
/// Used for transforming Rojo instance IDs into something deterministic. /// Used for transforming Rojo instance IDs into something deterministic.
#[derive(Default)]
pub struct RedactionMap { pub struct RedactionMap {
ids: HashMap<String, usize>, ids: HashMap<String, usize>,
last_id: usize, last_id: usize,
} }
impl RedactionMap { impl RedactionMap {
pub fn new() -> Self {
Self {
ids: HashMap::new(),
last_id: 0,
}
}
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> { pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
let id = id.to_string(); let id = id.to_string();
@@ -22,12 +28,6 @@ impl RedactionMap {
} }
} }
/// Returns the numeric ID that was assigned to the provided value,
/// if one exists.
pub fn get_id_for_value(&self, value: impl ToString) -> Option<usize> {
self.ids.get(&value.to_string()).cloned()
}
pub fn intern(&mut self, id: impl ToString) { pub fn intern(&mut self, id: impl ToString) {
let last_id = &mut self.last_id; let last_id = &mut self.last_id;

View File

@@ -0,0 +1,16 @@
[package]
name = "rojo-project"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.57"
globset = { version = "0.4.8", features = ["serde1"] }
log = "0.4.17"
rbx_dom_weak = "2.3.0"
rbx_reflection = "4.2.0"
rbx_reflection_database = "0.2.4"
serde = { version = "1.0.137", features = ["derive"] }
serde_json = "1.0.81"

View File

@@ -0,0 +1,4 @@
# rojo-project
Project file format crate for [Rojo].
[Rojo]: https://rojo.space

View File

@@ -1,6 +1,3 @@
//! Wrapper around globset's Glob type that has better serialization
//! characteristics by coupling Glob and GlobMatcher into a single type.
use std::path::Path; use std::path::Path;
use globset::{Glob as InnerGlob, GlobMatcher}; use globset::{Glob as InnerGlob, GlobMatcher};
@@ -8,6 +5,8 @@ use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
pub use globset::Error; pub use globset::Error;
/// Wrapper around globset's Glob type that has better serialization
/// characteristics by coupling Glob and GlobMatcher into a single type.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Glob { pub struct Glob {
inner: InnerGlob, inner: InnerGlob,
@@ -43,8 +42,8 @@ impl Serialize for Glob {
impl<'de> Deserialize<'de> for Glob { impl<'de> Deserialize<'de> for Glob {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let glob = String::deserialize(deserializer)?; let glob = <&str as Deserialize>::deserialize(deserializer)?;
Glob::new(&glob).map_err(D::Error::custom) Glob::new(glob).map_err(D::Error::custom)
} }
} }

View File

@@ -0,0 +1,7 @@
pub mod glob;
mod path_serializer;
mod project;
mod resolution;
pub use project::{OptionalPathNode, PathNode, Project, ProjectNode};
pub use resolution::{AmbiguousValue, UnresolvedValue};

View File

@@ -0,0 +1,21 @@
//! Path serializer is used to serialize absolute paths in a cross-platform way,
//! by replacing all directory separators with /.
use std::path::Path;
use serde::Serializer;
pub fn serialize_absolute<S, T>(path: T, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<Path>,
{
let as_str = path
.as_ref()
.as_os_str()
.to_str()
.expect("Invalid Unicode in file path, cannot serialize");
let replaced = as_str.replace("\\", "/");
serializer.serialize_str(&replaced)
}

View File

@@ -0,0 +1,363 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::glob::Glob;
use crate::resolution::UnresolvedValue;
static PROJECT_FILENAME: &str = "default.project.json";
/// Contains all of the configuration for a Rojo-managed project.
///
/// Rojo project files are stored in `.project.json` files.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Project {
/// The name of the top-level instance described by the project.
pub name: String,
/// The tree of instances described by this project. Projects always
/// describe at least one instance.
pub tree: ProjectNode,
/// If specified, sets the default port that `rojo serve` should use when
/// using this project for live sync.
///
/// Can be overriden with the `--port` flag.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_port: Option<u16>,
/// If specified, sets the default IP address that `rojo serve` should use
/// when using this project for live sync.
///
/// Can be overridden with the `--address` flag.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_address: Option<IpAddr>,
/// If specified, contains the set of place IDs that this project is
/// compatible with when doing live sync.
///
/// This setting is intended to help prevent syncing a Rojo project into the
/// wrong Roblox place.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_place_ids: Option<HashSet<u64>>,
/// If specified, sets the current place's place ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub place_id: Option<u64>,
/// If specified, sets the current place's game ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub game_id: Option<u64>,
/// A list of globs, relative to the folder the project file is in, that
/// match files that should be excluded if Rojo encounters them.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub glob_ignore_paths: Vec<Glob>,
/// The path to the file that this project came from. Relative paths in the
/// project should be considered relative to the parent of this field, also
/// given by `Project::folder_location`.
#[serde(skip)]
pub file_location: PathBuf,
}
impl Project {
/// Tells whether the given path describes a Rojo project.
pub fn is_project_file(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with(".project.json"))
.unwrap_or(false)
}
/// Loads a project file from a slice and a path that indicates where the
/// project should resolve paths relative to.
pub fn load_from_slice(contents: &[u8], project_file_location: &Path) -> anyhow::Result<Self> {
let mut project: Self = serde_json::from_slice(&contents).with_context(|| {
format!(
"Error parsing Rojo project at {}",
project_file_location.display()
)
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
/// Fuzzy-find a Rojo project and load it.
pub fn load_fuzzy(fuzzy_project_location: &Path) -> anyhow::Result<Option<Self>> {
if let Some(project_path) = Self::locate(fuzzy_project_location) {
let project = Self::load_exact(&project_path)?;
Ok(Some(project))
} else {
Ok(None)
}
}
/// Gives the path that all project file paths should resolve relative to.
pub fn folder_location(&self) -> &Path {
self.file_location.parent().unwrap()
}
/// Attempt to locate a project represented by the given path.
///
/// This will find a project if the path refers to a `.project.json` file,
/// or is a folder that contains a `default.project.json` file.
fn locate(path: &Path) -> Option<PathBuf> {
let meta = fs::metadata(path).ok()?;
if meta.is_file() {
if Project::is_project_file(path) {
Some(path.to_path_buf())
} else {
None
}
} else {
let child_path = path.join(PROJECT_FILENAME);
let child_meta = fs::metadata(&child_path).ok()?;
if child_meta.is_file() {
Some(child_path)
} else {
// This is a folder with the same name as a Rojo default project
// file.
//
// That's pretty weird, but we can roll with it.
None
}
}
}
fn load_exact(project_file_location: &Path) -> anyhow::Result<Self> {
let contents = fs::read_to_string(project_file_location)?;
let mut project: Project = serde_json::from_str(&contents).with_context(|| {
format!(
"Error parsing Rojo project at {}",
project_file_location.display()
)
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
/// Checks if there are any compatibility issues with this project file and
/// warns the user if there are any.
fn check_compatibility(&self) {
self.tree.validate_reserved_names();
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct OptionalPathNode {
#[serde(serialize_with = "crate::path_serializer::serialize_absolute")]
pub optional: PathBuf,
}
impl OptionalPathNode {
pub fn new(optional: PathBuf) -> Self {
OptionalPathNode { optional }
}
}
/// Describes a path that is either optional or required
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PathNode {
Required(#[serde(serialize_with = "crate::path_serializer::serialize_absolute")] PathBuf),
Optional(OptionalPathNode),
}
impl PathNode {
pub fn path(&self) -> &Path {
match self {
PathNode::Required(pathbuf) => &pathbuf,
PathNode::Optional(OptionalPathNode { optional }) => &optional,
}
}
}
/// Describes an instance and its descendants in a project.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ProjectNode {
/// If set, defines the ClassName of the described instance.
///
/// `$className` MUST be set if `$path` is not set.
///
/// `$className` CANNOT be set if `$path` is set and the instance described
/// by that path has a ClassName other than Folder.
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
/// Contains all of the children of the described instance.
#[serde(flatten)]
pub children: BTreeMap<String, ProjectNode>,
/// The properties that will be assigned to the resulting instance.
#[serde(
rename = "$properties",
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub properties: HashMap<String, UnresolvedValue>,
/// Defines the behavior when Rojo encounters unknown instances in Roblox
/// Studio during live sync. `$ignoreUnknownInstances` should be considered
/// a large hammer and used with care.
///
/// If set to `true`, those instances will be left alone. This may cause
/// issues when files that turn into instances are removed while Rojo is not
/// running.
///
/// If set to `false`, Rojo will destroy any instances it does not
/// recognize.
///
/// If unset, its default value depends on other settings:
/// - If `$path` is not set, defaults to `true`
/// - If `$path` is set, defaults to `false`
#[serde(
rename = "$ignoreUnknownInstances",
skip_serializing_if = "Option::is_none"
)]
pub ignore_unknown_instances: Option<bool>,
/// Defines that this instance should come from the given file path. This
/// path can point to any file type supported by Rojo, including Lua files
/// (`.lua`), Roblox models (`.rbxm`, `.rbxmx`), and localization table
/// spreadsheets (`.csv`).
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
pub path: Option<PathNode>,
}
impl ProjectNode {
fn validate_reserved_names(&self) {
for (name, child) in &self.children {
if name.starts_with('$') {
log::warn!(
"Keys starting with '$' are reserved by Rojo to ensure forward compatibility."
);
log::warn!(
"This project uses the key '{}', which should be renamed.",
name
);
}
child.validate_reserved_names();
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn path_node_required() {
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
}
#[test]
fn path_node_optional() {
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
assert_eq!(
path_node,
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
);
}
#[test]
fn project_node_required() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "src"
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Required(PathBuf::from("src")))
);
}
#[test]
fn project_node_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "src" }
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Optional(OptionalPathNode::new(PathBuf::from(
"src"
))))
);
}
#[test]
fn project_node_none() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$className": "Folder"
}"#,
)
.unwrap();
assert_eq!(project_node.path, None);
}
#[test]
fn project_node_optional_serialize_absolute() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "..\\src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_absolute_no_change() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "../src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "..\\src"
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":"../src"}"#);
}
}

View File

@@ -0,0 +1,294 @@
use std::borrow::Borrow;
use anyhow::format_err;
use rbx_dom_weak::types::{
CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3,
};
use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize};
/// A user-friendly version of `Variant` that supports specifying ambiguous
/// values. Ambiguous values need a reflection database to be resolved to a
/// usable value.
///
/// This type is used in Rojo projects and JSON models to make specifying the
/// most common types of properties, like strings or vectors, much easier.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UnresolvedValue {
FullyQualified(Variant),
Ambiguous(AmbiguousValue),
}
impl UnresolvedValue {
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
match self {
UnresolvedValue::FullyQualified(full) => Ok(full),
UnresolvedValue::Ambiguous(partial) => partial.resolve(class_name, prop_name),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AmbiguousValue {
Bool(bool),
String(String),
StringArray(Vec<String>),
Number(f64),
Array2([f64; 2]),
Array3([f64; 3]),
Array4([f64; 4]),
Array12([f64; 12]),
}
impl AmbiguousValue {
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
let property = find_descriptor(class_name, prop_name)
.ok_or_else(|| format_err!("Unknown property {}.{}", class_name, prop_name))?;
match &property.data_type {
DataType::Enum(enum_name) => {
let database = rbx_reflection_database::get();
let enum_descriptor = database.enums.get(enum_name).ok_or_else(|| {
format_err!("Unknown enum {}. This is a Rojo bug!", enum_name)
})?;
let error = |what: &str| {
let mut all_values = enum_descriptor
.items
.keys()
.map(|value| value.borrow())
.collect::<Vec<_>>();
all_values.sort();
let examples = nonexhaustive_list(&all_values);
format_err!(
"Invalid value for property {}.{}. Got {} but \
expected a member of the {} enum such as {}",
class_name,
prop_name,
what,
enum_name,
examples,
)
};
let value = match self {
AmbiguousValue::String(value) => value,
unresolved => return Err(error(unresolved.describe())),
};
let resolved = enum_descriptor
.items
.get(value.as_str())
.ok_or_else(|| error(value.as_str()))?;
Ok(Enum::from_u32(*resolved).into())
}
DataType::Value(variant_ty) => match (variant_ty, self) {
(VariantType::Bool, AmbiguousValue::Bool(value)) => Ok(value.into()),
(VariantType::Float32, AmbiguousValue::Number(value)) => Ok((value as f32).into()),
(VariantType::Float64, AmbiguousValue::Number(value)) => Ok(value.into()),
(VariantType::Int32, AmbiguousValue::Number(value)) => Ok((value as i32).into()),
(VariantType::Int64, AmbiguousValue::Number(value)) => Ok((value as i64).into()),
(VariantType::String, AmbiguousValue::String(value)) => Ok(value.into()),
(VariantType::Tags, AmbiguousValue::StringArray(value)) => {
Ok(Tags::from(value).into())
}
(VariantType::Content, AmbiguousValue::String(value)) => {
Ok(Content::from(value).into())
}
(VariantType::Vector2, AmbiguousValue::Array2(value)) => {
Ok(Vector2::new(value[0] as f32, value[1] as f32).into())
}
(VariantType::Vector3, AmbiguousValue::Array3(value)) => {
Ok(Vector3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
}
(VariantType::Color3, AmbiguousValue::Array3(value)) => {
Ok(Color3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
}
(VariantType::CFrame, AmbiguousValue::Array12(value)) => {
let value = value.map(|v| v as f32);
let pos = Vector3::new(value[0], value[1], value[2]);
let orientation = Matrix3::new(
Vector3::new(value[3], value[4], value[5]),
Vector3::new(value[6], value[7], value[8]),
Vector3::new(value[9], value[10], value[11]),
);
Ok(CFrame::new(pos, orientation).into())
}
(_, unresolved) => Err(format_err!(
"Wrong type of value for property {}.{}. Expected {:?}, got {}",
class_name,
prop_name,
variant_ty,
unresolved.describe(),
)),
},
_ => Err(format_err!(
"Unknown data type for property {}.{}",
class_name,
prop_name
)),
}
}
fn describe(&self) -> &'static str {
match self {
AmbiguousValue::Bool(_) => "a bool",
AmbiguousValue::String(_) => "a string",
AmbiguousValue::StringArray(_) => "an array of strings",
AmbiguousValue::Number(_) => "a number",
AmbiguousValue::Array2(_) => "an array of two numbers",
AmbiguousValue::Array3(_) => "an array of three numbers",
AmbiguousValue::Array4(_) => "an array of four numbers",
AmbiguousValue::Array12(_) => "an array of twelve numbers",
}
}
}
fn find_descriptor(
class_name: &str,
prop_name: &str,
) -> Option<&'static PropertyDescriptor<'static>> {
let database = rbx_reflection_database::get();
let mut current_class_name = class_name;
loop {
let class = database.classes.get(current_class_name)?;
if let Some(descriptor) = class.properties.get(prop_name) {
return Some(descriptor);
}
current_class_name = class.superclass.as_deref()?;
}
}
/// Outputs a string containing up to MAX_ITEMS entries from the given list. If
/// there are more than MAX_ITEMS items, the number of remaining items will be
/// listed.
fn nonexhaustive_list(values: &[&str]) -> String {
use std::fmt::Write;
const MAX_ITEMS: usize = 8;
let mut output = String::new();
let last_index = values.len() - 1;
let main_length = last_index.min(9);
let main_list = &values[..main_length];
for value in main_list {
output.push_str(value);
output.push_str(", ");
}
if values.len() > MAX_ITEMS {
write!(output, "or {} more", values.len() - main_length).unwrap();
} else {
output.push_str("or ");
output.push_str(values[values.len() - 1]);
}
output
}
#[cfg(test)]
mod test {
use super::*;
fn resolve(class: &str, prop: &str, json_value: &str) -> Variant {
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
unresolved.resolve(class, prop).unwrap()
}
#[test]
fn bools() {
assert_eq!(resolve("BoolValue", "Value", "false"), Variant::Bool(false));
// Script.Disabled is inherited from BaseScript
assert_eq!(resolve("Script", "Disabled", "true"), Variant::Bool(true));
}
#[test]
fn strings() {
// String literals can stay as strings
assert_eq!(
resolve("StringValue", "Value", "\"Hello!\""),
Variant::String("Hello!".into()),
);
// String literals can also turn into Content
assert_eq!(
resolve("Sky", "MoonTextureId", "\"rbxassetid://12345\""),
Variant::Content("rbxassetid://12345".into()),
);
// What about BinaryString values? For forward-compatibility reasons, we
// don't support any shorthands for BinaryString.
//
// assert_eq!(
// resolve("Folder", "Tags", "\"a\\u0000b\\u0000c\""),
// Variant::BinaryString(b"a\0b\0c".to_vec().into()),
// );
}
#[test]
fn numbers() {
assert_eq!(
resolve("Part", "CollisionGroupId", "123"),
Variant::Int32(123),
);
assert_eq!(
resolve("Folder", "SourceAssetId", "532413"),
Variant::Int64(532413),
);
assert_eq!(resolve("Part", "Transparency", "1"), Variant::Float32(1.0));
assert_eq!(resolve("NumberValue", "Value", "1"), Variant::Float64(1.0));
}
#[test]
fn vectors() {
assert_eq!(
resolve("ParticleEmitter", "SpreadAngle", "[1, 2]"),
Variant::Vector2(Vector2::new(1.0, 2.0)),
);
assert_eq!(
resolve("Part", "Position", "[4, 5, 6]"),
Variant::Vector3(Vector3::new(4.0, 5.0, 6.0)),
);
}
#[test]
fn colors() {
assert_eq!(
resolve("Part", "Color", "[1, 1, 1]"),
Variant::Color3(Color3::new(1.0, 1.0, 1.0)),
);
// There aren't any user-facing Color3uint8 properties. If there are
// some, we should treat them the same in the future.
}
#[test]
fn enums() {
assert_eq!(
resolve("Lighting", "Technology", "\"Voxel\""),
Variant::Enum(Enum::from_u32(1)),
);
}
}

4
foreman.toml Normal file
View File

@@ -0,0 +1,4 @@
[tools]
rojo = { source = "rojo-rbx/rojo", version = "7.1.1" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
selene = { source = "Kampfkarren/selene", version = "0.17.0" }

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.6.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

@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
elseif valueType == "table" then elseif valueType == "table" then
local valueMeta = getmetatable(value) local valueMeta = getmetatable(value)
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
-- This type implement's the metamethod we made up to line up with -- This type implement's the metamethod we made up to line up with
-- Rust's 'Debug' trait. -- Rust's 'Debug' trait.
@@ -242,4 +242,4 @@ return {
debugOutputBuffer = debugOutputBuffer, debugOutputBuffer = debugOutputBuffer,
fmt = fmt, fmt = fmt,
debugify = debugify, debugify = debugify,
} }

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

@@ -31,4 +31,4 @@ function Response:json()
return HttpService:JSONDecode(self.body) return HttpService:JSONDecode(self.body)
end end
return Response return Response

View File

@@ -30,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))
@@ -68,4 +63,4 @@ function Http.jsonDecode(source)
return HttpService:JSONDecode(source) return HttpService:JSONDecode(source)
end end
return Http return Http

View File

@@ -2,4 +2,4 @@ return function()
it("should load", function() it("should load", function()
require(script.Parent) require(script.Parent)
end) end)
end end

View File

@@ -57,4 +57,4 @@ function Log.error(template, ...)
error(Fmt.fmt(template, ...)) error(Fmt.fmt(template, ...))
end end
return Log return Log

View File

@@ -2,4 +2,4 @@ return function()
it("should load", function() it("should load", function()
require(script.Parent) require(script.Parent)
end) end)
end end

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 25d957d4d5

View File

@@ -20,54 +20,11 @@ local function serializeFloat(value)
return value return value
end end
local ALL_AXES = { "X", "Y", "Z" } local ALL_AXES = {"X", "Y", "Z"}
local ALL_FACES = { "Right", "Top", "Back", "Left", "Bottom", "Front" } local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
local EncodedValue = {}
local types local types
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 = { Axes = {
fromPod = function(pod) fromPod = function(pod)
local axes = {} local axes = {}
@@ -117,7 +74,6 @@ types = {
local pos = pod.position local pos = pod.position
local orient = pod.orientation local orient = pod.orientation
--stylua: ignore
return CFrame.new( return CFrame.new(
pos[1], pos[2], pos[3], pos[1], pos[2], pos[3],
orient[1][1], orient[1][2], orient[1][3], orient[1][1], orient[1][2], orient[1][3],
@@ -127,14 +83,17 @@ types = {
end, end,
toPod = function(roblox) toPod = function(roblox)
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = roblox:GetComponents() local x, y, z,
r00, r01, r02,
r10, r11, r12,
r20, r21, r22 = roblox:GetComponents()
return { return {
position = { x, y, z }, position = {x, y, z},
orientation = { orientation = {
{ r00, r01, r02 }, {r00, r01, r02},
{ r10, r11, r12 }, {r10, r11, r12},
{ r20, r21, r22 }, {r20, r21, r22},
}, },
} }
end, end,
@@ -144,7 +103,7 @@ types = {
fromPod = unpackDecoder(Color3.new), fromPod = unpackDecoder(Color3.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.r, roblox.g, roblox.b } return {roblox.r, roblox.g, roblox.b}
end, end,
}, },
@@ -165,7 +124,10 @@ types = {
local keypoints = {} local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = ColorSequenceKeypoint.new(keypoint.time, types.Color3.fromPod(keypoint.color)) keypoints[index] = ColorSequenceKeypoint.new(
keypoint.time,
types.Color3.fromPod(keypoint.color)
)
end end
return ColorSequence.new(keypoints) return ColorSequence.new(keypoints)
@@ -188,38 +150,6 @@ types = {
}, },
Content = { 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, fromPod = identity,
toPod = identity, toPod = identity,
}, },
@@ -237,19 +167,6 @@ types = {
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 = { Faces = {
fromPod = function(pod) fromPod = function(pod)
local faces = {} local faces = {}
@@ -284,23 +201,6 @@ types = {
toPod = serializeFloat, 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 = { Int32 = {
fromPod = identity, fromPod = identity,
toPod = identity, toPod = identity,
@@ -311,32 +211,11 @@ types = {
toPod = 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 = { NumberRange = {
fromPod = unpackDecoder(NumberRange.new), fromPod = unpackDecoder(NumberRange.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.Min, roblox.Max } return {roblox.Min, roblox.Max}
end, end,
}, },
@@ -345,12 +224,11 @@ types = {
local keypoints = {} local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do for index, keypoint in ipairs(pod.keypoints) do
-- TODO: Add a test for NaN or Infinity values and envelopes keypoints[index] = NumberSequenceKeypoint.new(
-- Right now it isn't possible because it'd fail the roundtrip. keypoint.time,
-- It's more important that it works right now, though. keypoint.value,
local value = keypoint.value or 0 keypoint.envelope
local envelope = keypoint.envelope or 0 )
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
end end
return NumberSequence.new(keypoints) return NumberSequence.new(keypoints)
@@ -378,26 +256,13 @@ types = {
if pod == "Default" then if pod == "Default" then
return nil return nil
else else
-- Passing `nil` instead of not passing anything gives return PhysicalProperties.new(
-- different results, so we have to branch here. pod.density,
if pod.acousticAbsorption then pod.friction,
return (PhysicalProperties.new :: any)( pod.elasticity,
pod.density, pod.frictionWeight,
pod.friction, pod.elasticityWeight
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
end, end,
@@ -411,7 +276,6 @@ types = {
elasticity = roblox.Elasticity, elasticity = roblox.Elasticity,
frictionWeight = roblox.FrictionWeight, frictionWeight = roblox.FrictionWeight,
elasticityWeight = roblox.ElasticityWeight, elasticityWeight = roblox.ElasticityWeight,
acousticAbsorption = roblox.AcousticAbsorption,
} }
end end
end, end,
@@ -419,7 +283,10 @@ types = {
Ray = { Ray = {
fromPod = function(pod) fromPod = function(pod)
return Ray.new(types.Vector3.fromPod(pod.origin), types.Vector3.fromPod(pod.direction)) return Ray.new(
types.Vector3.fromPod(pod.origin),
types.Vector3.fromPod(pod.direction)
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -432,7 +299,10 @@ types = {
Rect = { Rect = {
fromPod = function(pod) fromPod = function(pod)
return Rect.new(types.Vector2.fromPod(pod[1]), types.Vector2.fromPod(pod[2])) return Rect.new(
types.Vector2.fromPod(pod[1]),
types.Vector2.fromPod(pod[2])
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -444,28 +314,31 @@ types = {
}, },
Ref = { Ref = {
fromPod = function(_) fromPod = function(_pod)
error("Ref cannot be decoded on its own") error("Ref cannot be decoded on its own")
end, end,
toPod = function(_) toPod = function(_roblox)
error("Ref can not be encoded on its own") error("Ref can not be encoded on its own")
end, end,
}, },
Region3 = { Region3 = {
fromPod = function(_) fromPod = function(pod)
error("Region3 is not implemented") error("Region3 is not implemented")
end, end,
toPod = function(_) toPod = function(roblox)
error("Region3 is not implemented") error("Region3 is not implemented")
end, end,
}, },
Region3int16 = { Region3int16 = {
fromPod = function(pod) fromPod = function(pod)
return Region3int16.new(types.Vector3int16.fromPod(pod[1]), types.Vector3int16.fromPod(pod[2])) return Region3int16.new(
types.Vector3int16.fromPod(pod[1]),
types.Vector3int16.fromPod(pod[2])
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -477,11 +350,11 @@ types = {
}, },
SharedString = { SharedString = {
fromPod = function(_pod) fromPod = function(pod)
error("SharedString is not supported") error("SharedString is not supported")
end, end,
toPod = function(_roblox) toPod = function(roblox)
error("SharedString is not supported") error("SharedString is not supported")
end, end,
}, },
@@ -495,13 +368,16 @@ types = {
fromPod = unpackDecoder(UDim.new), fromPod = unpackDecoder(UDim.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.Scale, roblox.Offset } return {roblox.Scale, roblox.Offset}
end, end,
}, },
UDim2 = { UDim2 = {
fromPod = function(pod) fromPod = function(pod)
return UDim2.new(types.UDim.fromPod(pod[1]), types.UDim.fromPod(pod[2])) return UDim2.new(
types.UDim.fromPod(pod[1]),
types.UDim.fromPod(pod[2])
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -532,7 +408,7 @@ types = {
fromPod = unpackDecoder(Vector2int16.new), fromPod = unpackDecoder(Vector2int16.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.X, roblox.Y } return {roblox.X, roblox.Y}
end, end,
}, },
@@ -552,37 +428,16 @@ types = {
fromPod = unpackDecoder(Vector3int16.new), fromPod = unpackDecoder(Vector3int16.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.X, roblox.Y, roblox.Z } return {roblox.X, roblox.Y, roblox.Z}
end, end,
}, },
} }
types.OptionalCFrame = { local EncodedValue = {}
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) function EncodedValue.decode(encodedValue)
local ty, value = next(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] local typeImpl = types[ty]
if typeImpl == nil then if typeImpl == nil then
return false, "Couldn't decode value " .. tostring(ty) return false, "Couldn't decode value " .. tostring(ty)
@@ -604,19 +459,4 @@ function EncodedValue.encode(rbxValue, propertyType)
} }
end 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 return EncodedValue

View File

@@ -0,0 +1,72 @@
return function()
local HttpService = game:GetService("HttpService")
local EncodedValue = require(script.Parent.EncodedValue)
local allValues = require(script.Parent.allValues)
local function deepEq(a, b)
if typeof(a) ~= typeof(b) then
return false
end
local ty = typeof(a)
if ty == "table" then
local visited = {}
for key, valueA in pairs(a) do
visited[key] = true
if not deepEq(valueA, b[key]) then
return false
end
end
for key, valueB in pairs(b) do
if visited[key] then
continue
end
if not deepEq(valueB, a[key]) then
return false
end
end
return true
else
return a == b
end
end
local extraAssertions = {
CFrame = function(value)
expect(value).to.equal(CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
end,
}
for testName, testEntry in pairs(allValues) do
it("round trip " .. testName, function()
local ok, decoded = EncodedValue.decode(testEntry.value)
assert(ok, decoded)
if extraAssertions[testName] ~= nil then
extraAssertions[testName](decoded)
end
local ok, encoded = EncodedValue.encode(decoded, testEntry.ty)
assert(ok, encoded)
if not deepEq(encoded, testEntry.value) then
local expected = HttpService:JSONEncode(testEntry.value)
local actual = HttpService:JSONEncode(encoded)
local message = string.format(
"Round-trip results did not match.\nExpected:\n%s\nActual:\n%s",
expected, actual
)
error(message)
end
end)
end
end

View File

@@ -5,7 +5,6 @@ Error.Kind = {
UnknownProperty = "UnknownProperty", UnknownProperty = "UnknownProperty",
PropertyNotReadable = "PropertyNotReadable", PropertyNotReadable = "PropertyNotReadable",
PropertyNotWritable = "PropertyNotWritable", PropertyNotWritable = "PropertyNotWritable",
CannotParseBinaryString = "CannotParseBinaryString",
Roblox = "Roblox", Roblox = "Roblox",
} }
@@ -26,4 +25,4 @@ function Error:__tostring()
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra)) return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
end end
return Error return Error

View File

@@ -53,11 +53,6 @@ function PropertyDescriptor:read(instance)
end end
if self.scriptability == "Custom" then if self.scriptability == "Custom" then
if customProperties[self.className] == nil then
local fullName = ("%s.%s"):format(instance.className, self.name)
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
end
local interface = customProperties[self.className][self.name] local interface = customProperties[self.className][self.name]
return interface.read(instance, self.name) return interface.read(instance, self.name)
@@ -84,11 +79,6 @@ function PropertyDescriptor:write(instance, value)
end end
if self.scriptability == "Custom" then if self.scriptability == "Custom" then
if customProperties[self.className] == nil then
local fullName = ("%s.%s"):format(instance.className, self.name)
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
end
local interface = customProperties[self.className][self.name] local interface = customProperties[self.className][self.name]
return interface.write(instance, self.name, value) return interface.write(instance, self.name, value)

View File

@@ -1,79 +1,4 @@
{ {
"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": { "Axes": {
"value": { "value": {
"Axes": [ "Axes": [
@@ -176,23 +101,9 @@
}, },
"ty": "ColorSequence" "ty": "ColorSequence"
}, },
"ContentId": { "Content": {
"value": { "value": {
"ContentId": "rbxassetid://12345" "Content": "rbxassetid://12345"
},
"ty": "ContentId"
},
"Content_None": {
"value": {
"Content": "None"
},
"ty": "Content"
},
"Content_Uri": {
"value": {
"Content": {
"Uri": "rbxasset://abc/123.rojo"
}
}, },
"ty": "Content" "ty": "Content"
}, },
@@ -202,15 +113,6 @@
}, },
"ty": "Enum" "ty": "Enum"
}, },
"EnumItem": {
"value": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"ty": "EnumItem"
},
"Faces": { "Faces": {
"value": { "value": {
"Faces": [ "Faces": [
@@ -236,17 +138,6 @@
}, },
"ty": "Float64" "ty": "Float64"
}, },
"Font": {
"value": {
"Font": {
"family": "rbxasset://fonts/families/SourceSansPro.json",
"weight": "Regular",
"style": "Normal",
"cachedFaceId": null
}
},
"ty": "Font"
},
"Int32": { "Int32": {
"value": { "value": {
"Int32": 6014 "Int32": 6014
@@ -259,118 +150,6 @@
}, },
"ty": "Int64" "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": { "NumberRange": {
"value": { "value": {
"NumberRange": [ "NumberRange": [
@@ -399,41 +178,6 @@
}, },
"ty": "NumberSequence" "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": { "PhysicalProperties-Custom": {
"value": { "value": {
"PhysicalProperties": { "PhysicalProperties": {
@@ -441,8 +185,7 @@
"friction": 1.0, "friction": 1.0,
"elasticity": 0.0, "elasticity": 0.0,
"frictionWeight": 50.0, "frictionWeight": 50.0,
"elasticityWeight": 25.0, "elasticityWeight": 25.0
"acousticAbsorption": 0.15625
} }
}, },
"ty": "PhysicalProperties" "ty": "PhysicalProperties"

View File

@@ -136,4 +136,4 @@ end
return { return {
decode = decodeBase64, decode = decodeBase64,
encode = encodeBase64, 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

@@ -1,99 +1,10 @@
local CollectionService = game:GetService("CollectionService") 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. -- Defines how to read and write properties that aren't directly scriptable.
-- --
-- The reflection database refers to these as having scriptability = "Custom" -- The reflection database refers to these as having scriptability = "Custom"
return { return {
Instance = { 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 = { Tags = {
read = function(instance) read = function(instance)
return true, CollectionService:GetTags(instance) return true, CollectionService:GetTags(instance)
@@ -121,91 +32,13 @@ return {
}, },
LocalizationTable = { LocalizationTable = {
Contents = { Contents = {
read = function(instance, _) read = function(instance, key)
return true, instance:GetContents() return true, instance:GetContents()
end, end,
write = function(instance, _, value) write = function(instance, key, value)
instance:SetContents(value) instance:SetContents(value)
return true return true
end, 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,
},
},
} }

File diff suppressed because it is too large Load Diff

View File

@@ -24,8 +24,7 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
return PropertyDescriptor.fromRaw( return PropertyDescriptor.fromRaw(
currentClass.Properties[aliasData.AliasFor], currentClass.Properties[aliasData.AliasFor],
currentClassName, currentClassName,
aliasData.AliasFor aliasData.AliasFor)
)
end end
return nil return nil

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

@@ -1,11 +1,19 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10)) local TestEZ = require(ReplicatedStorage.TestEZ)
local Rojo = ReplicatedStorage.Rojo local Rojo = ReplicatedStorage.Rojo
local Settings = require(Rojo.Plugin.Settings) local DevSettings = require(Rojo.Plugin.DevSettings)
Settings:set("logLevel", "Trace")
Settings:set("typecheckingEnabled", true) local setDevSettings = not DevSettings:hasChangedValues()
if setDevSettings then
DevSettings:createTestSettings()
end
require(Rojo.Plugin.runTests)(TestEZ) require(Rojo.Plugin.runTests)(TestEZ)
if setDevSettings then
DevSettings:resetValues()
end

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