Compare commits
1 Commits
name-prop
...
project-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b96a236333 |
@@ -1,2 +0,0 @@
|
||||
((nil . ((eglot-luau-rojo-project-path . "plugin.project.json")
|
||||
(eglot-luau-rojo-sourcemap-enabled . 't))))
|
||||
@@ -23,7 +23,4 @@ insert_final_newline = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.lua]
|
||||
indent_style = tab
|
||||
|
||||
[*.luau]
|
||||
indent_style = tab
|
||||
@@ -1,2 +0,0 @@
|
||||
# stylua formatting
|
||||
0f8e1625d572a5fe0f7b5c08653ff92cc837d346
|
||||
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.lua linguist-language=Luau
|
||||
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
patreon: lpghatguy
|
||||
23
.github/workflows/changelog.yml
vendored
@@ -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 }}
|
||||
100
.github/workflows/ci.yml
vendored
@@ -12,28 +12,23 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm]
|
||||
rust_version: [stable, 1.55.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
toolchain: ${{ matrix.rust_version }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
@@ -41,93 +36,24 @@ jobs:
|
||||
- name: Test
|
||||
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:
|
||||
name: Rustfmt, Clippy, Stylua, & Selene
|
||||
name: Rustfmt and Clippy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Setup Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
with:
|
||||
version: 'v1.1.0'
|
||||
|
||||
- name: Stylua
|
||||
run: stylua --check plugin/src
|
||||
|
||||
- name: Selene
|
||||
run: selene plugin/src
|
||||
|
||||
- name: Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
run: cargo clippy
|
||||
131
.github/workflows/release.yml
vendored
@@ -8,39 +8,49 @@ jobs:
|
||||
create-release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
build-plugin:
|
||||
needs: ["create-release"]
|
||||
name: Build Roblox Studio Plugin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
- name: Setup Foreman
|
||||
uses: Roblox/setup-foreman@v1
|
||||
with:
|
||||
version: 'v1.1.0'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Plugin
|
||||
run: rojo build plugin.project.json --output Rojo.rbxm
|
||||
run: rojo build plugin --output Rojo.rbxm
|
||||
|
||||
- name: Upload Plugin to Release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} Rojo.rbxm
|
||||
with:
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rojo.rbxm
|
||||
path: Rojo.rbxm
|
||||
@@ -51,31 +61,25 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# 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:
|
||||
- host: linux
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
label: linux-x86_64
|
||||
|
||||
- host: linux
|
||||
os: ubuntu-22.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
label: linux-aarch64
|
||||
label: linux
|
||||
|
||||
- host: windows
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
label: windows-x86_64
|
||||
|
||||
- host: windows
|
||||
os: windows-11-arm
|
||||
target: aarch64-pc-windows-msvc
|
||||
label: windows-aarch64
|
||||
label: win64
|
||||
|
||||
- host: macos
|
||||
os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
label: macos-x86_64
|
||||
label: macos
|
||||
|
||||
- host: macos
|
||||
os: macos-latest
|
||||
@@ -87,64 +91,63 @@ jobs:
|
||||
env:
|
||||
BIN: rojo
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Get Version from Tag
|
||||
shell: bash
|
||||
# https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
|
||||
run: |
|
||||
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
echo "Version is: ${{ env.PROJECT_VERSION }}"
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
toolchain: stable
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Build Release
|
||||
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Generate Artifact Name
|
||||
shell: bash
|
||||
run: cargo build --release --locked --verbose
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV"
|
||||
# Build into a known directory so we can find our build artifact more
|
||||
# easily.
|
||||
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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mkdir staging
|
||||
|
||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||
cp "target/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||
cp "output/release/$BIN.exe" staging/
|
||||
cd staging
|
||||
7z a ../$ARTIFACT_NAME *
|
||||
7z a ../release.zip *
|
||||
else
|
||||
cp "target/${{ matrix.target }}/release/$BIN" staging/
|
||||
cp "output/release/$BIN" staging/
|
||||
cd staging
|
||||
zip ../$ARTIFACT_NAME *
|
||||
zip ../release.zip *
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: ${{ env.ARTIFACT_NAME }}
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
||||
path: release.zip
|
||||
12
.gitignore
vendored
@@ -10,8 +10,8 @@
|
||||
/*.rbxl
|
||||
/*.rbxlx
|
||||
|
||||
# Sourcemap for the Rojo plugin (for better intellisense)
|
||||
/sourcemap.json
|
||||
# Test places for the Roblox Studio Plugin
|
||||
/plugin/*.rbxlx
|
||||
|
||||
# Roblox Studio holds 'lock' files on places
|
||||
*.rbxl.lock
|
||||
@@ -20,9 +20,5 @@
|
||||
# Snapshot files from the 'insta' Rust crate
|
||||
**/*.snap.new
|
||||
|
||||
# Macos file system junk
|
||||
._*
|
||||
.DS_STORE
|
||||
|
||||
# JetBrains IDEs
|
||||
/.idea/
|
||||
# Selene generates a roblox.toml file that should not be checked in.
|
||||
/roblox.toml
|
||||
31
.gitmodules
vendored
@@ -1,18 +1,15 @@
|
||||
[submodule "plugin/Packages/Roact"]
|
||||
path = plugin/Packages/Roact
|
||||
url = https://github.com/roblox/roact.git
|
||||
[submodule "plugin/Packages/Flipper"]
|
||||
path = plugin/Packages/Flipper
|
||||
url = https://github.com/reselim/flipper.git
|
||||
[submodule "plugin/Packages/Promise"]
|
||||
path = plugin/Packages/Promise
|
||||
url = https://github.com/evaera/roblox-lua-promise.git
|
||||
[submodule "plugin/Packages/t"]
|
||||
path = plugin/Packages/t
|
||||
[submodule "plugin/modules/roact"]
|
||||
path = plugin/modules/roact
|
||||
url = https://github.com/Roblox/roact.git
|
||||
[submodule "plugin/modules/testez"]
|
||||
path = plugin/modules/testez
|
||||
url = https://github.com/Roblox/testez.git
|
||||
[submodule "plugin/modules/promise"]
|
||||
path = plugin/modules/promise
|
||||
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
||||
[submodule "plugin/modules/t"]
|
||||
path = plugin/modules/t
|
||||
url = https://github.com/osyrisrblx/t.git
|
||||
[submodule "plugin/Packages/TestEZ"]
|
||||
path = plugin/Packages/TestEZ
|
||||
url = https://github.com/roblox/testez.git
|
||||
[submodule "plugin/Packages/Highlighter"]
|
||||
path = plugin/Packages/Highlighter
|
||||
url = https://github.com/boatbomber/highlighter.git
|
||||
[submodule "plugin/modules/flipper"]
|
||||
path = plugin/modules/flipper
|
||||
url = https://github.com/Reselim/Flipper
|
||||
|
||||
58
.luacheckrc
Normal 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",
|
||||
}
|
||||
8
.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"JohnnyMorganz.luau-lsp",
|
||||
"JohnnyMorganz.stylua",
|
||||
"Kampfkarren.selene-vscode",
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"luau-lsp.sourcemap.rojoProjectFile": "plugin.project.json",
|
||||
"luau-lsp.sourcemap.autogenerate": true
|
||||
}
|
||||
1173
CHANGELOG.md
@@ -15,29 +15,12 @@ You'll want these tools to work on Rojo:
|
||||
|
||||
* Latest stable Rust compiler
|
||||
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
||||
* [Rokit](https://github.com/rojo-rbx/rokit)
|
||||
* [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
|
||||
```
|
||||
* [Foreman](https://github.com/Roblox/foreman)
|
||||
|
||||
## Documentation
|
||||
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
|
||||
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.
|
||||
|
||||
2619
Cargo.lock
generated
116
Cargo.toml
@@ -1,22 +1,19 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.7.0-rc.1"
|
||||
rust-version = "1.88"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
"Ken Loeffler <kenloef@gmail.com>",
|
||||
]
|
||||
version = "7.1.1"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "Enables professional-grade development tools for Roblox developers"
|
||||
license = "MPL-2.0"
|
||||
homepage = "https://rojo.space"
|
||||
documentation = "https://rojo.space/docs"
|
||||
repository = "https://github.com/rojo-rbx/rojo"
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
|
||||
exclude = ["/test-projects/**"]
|
||||
exclude = [
|
||||
"/test-projects/**",
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
@@ -30,10 +27,6 @@ default = []
|
||||
# Enable this feature to live-reload assets from the web UI.
|
||||
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]
|
||||
members = ["crates/*"]
|
||||
|
||||
@@ -46,85 +39,68 @@ name = "build"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
memofs = { version = "0.3.1", 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
|
||||
# rbx_binary = { path = "../rbx-dom/rbx_binary", features = [
|
||||
# "unstable_text_format",
|
||||
# ] }
|
||||
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
|
||||
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
|
||||
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
|
||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||
|
||||
rbx_binary = { version = "2.0.1", features = ["unstable_text_format"] }
|
||||
rbx_dom_weak = "4.1.0"
|
||||
rbx_reflection = "6.1.0"
|
||||
rbx_reflection_database = "2.0.2"
|
||||
rbx_xml = "2.0.1"
|
||||
rbx_binary = "0.6.4"
|
||||
rbx_dom_weak = "2.3.0"
|
||||
rbx_reflection = "4.2.0"
|
||||
rbx_reflection_database = "0.2.2"
|
||||
rbx_xml = "0.12.3"
|
||||
|
||||
anyhow = "1.0.80"
|
||||
backtrace = "0.3.69"
|
||||
anyhow = "1.0.44"
|
||||
backtrace = "0.3.61"
|
||||
bincode = "1.3.3"
|
||||
crossbeam-channel = "0.5.12"
|
||||
csv = "1.3.0"
|
||||
env_logger = "0.9.3"
|
||||
fs-err = "2.11.0"
|
||||
futures = "0.3.30"
|
||||
globset = "0.4.14"
|
||||
crossbeam-channel = "0.5.1"
|
||||
csv = "1.1.6"
|
||||
env_logger = "0.9.0"
|
||||
fs-err = "2.6.0"
|
||||
futures = "0.3.17"
|
||||
globset = "0.4.8"
|
||||
humantime = "2.1.0"
|
||||
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||
hyper-tungstenite = "0.11.0"
|
||||
hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
|
||||
jod-thread = "0.1.2"
|
||||
log = "0.4.21"
|
||||
num_cpus = "1.16.0"
|
||||
opener = "0.5.2"
|
||||
rayon = "1.9.0"
|
||||
reqwest = { version = "0.11.24", default-features = false, features = [
|
||||
"blocking",
|
||||
"json",
|
||||
"rustls-tls",
|
||||
] }
|
||||
log = "0.4.14"
|
||||
maplit = "1.0.2"
|
||||
notify = "4.0.17"
|
||||
opener = "0.5.0"
|
||||
reqwest = { version = "0.11.10", features = ["blocking", "json"] }
|
||||
ritz = "0.1.0"
|
||||
roblox_install = "1.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.145"
|
||||
jsonc-parser = { version = "0.27.0", features = ["serde"] }
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
toml = "0.5.11"
|
||||
termcolor = "1.4.1"
|
||||
thiserror = "1.0.57"
|
||||
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
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"
|
||||
|
||||
blake3 = "1.5.0"
|
||||
float-cmp = "0.9.0"
|
||||
indexmap = { version = "2.10.0", features = ["serde"] }
|
||||
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.68"
|
||||
termcolor = "1.1.2"
|
||||
thiserror = "1.0.30"
|
||||
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
|
||||
uuid = { version = "1.0.0", features = ["v4", "serde"] }
|
||||
clap = { version = "3.1.18", features = ["derive"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.10.1"
|
||||
|
||||
[build-dependencies]
|
||||
memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||
memofs = { version = "0.2.0", path = "crates/memofs" }
|
||||
|
||||
embed-resource = "1.8.0"
|
||||
anyhow = "1.0.80"
|
||||
embed-resource = "1.6.4"
|
||||
anyhow = "1.0.44"
|
||||
bincode = "1.3.3"
|
||||
fs-err = "2.11.0"
|
||||
fs-err = "2.6.0"
|
||||
maplit = "1.0.2"
|
||||
semver = "1.0.22"
|
||||
|
||||
[dev-dependencies]
|
||||
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
||||
|
||||
criterion = "0.3.6"
|
||||
insta = { version = "1.36.1", features = ["redactions", "yaml"] }
|
||||
paste = "1.0.14"
|
||||
pretty_assertions = "1.4.0"
|
||||
serde_yaml = "0.8.26"
|
||||
tempfile = "3.10.1"
|
||||
walkdir = "2.5.0"
|
||||
criterion = "0.3.5"
|
||||
insta = { version = "1.8.0", features = ["redactions"] }
|
||||
paste = "1.0.5"
|
||||
pretty_assertions = "1.2.1"
|
||||
serde_yaml = "0.8.21"
|
||||
tempfile = "3.2.0"
|
||||
walkdir = "2.3.2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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>
|
||||
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
Rojo supports Rust 1.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
Rojo supports Rust 1.46.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
|
||||
## License
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
@@ -2,7 +2,7 @@
|
||||
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||
|
||||
## Getting Started
|
||||
To build this library, use:
|
||||
To build this library or plugin, use:
|
||||
|
||||
```bash
|
||||
rojo build -o "{project_name}.rbxmx"
|
||||
@@ -1,5 +1,3 @@
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
sourcemap.json
|
||||
/*.rbxl.lock
|
||||
@@ -2,4 +2,4 @@ return {
|
||||
hello = function()
|
||||
print("Hello world, from {project_name}!")
|
||||
end,
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"$className": "DataModel",
|
||||
|
||||
"ReplicatedStorage": {
|
||||
"Shared": {
|
||||
"Common": {
|
||||
"$path": "src/shared"
|
||||
}
|
||||
},
|
||||
@@ -3,6 +3,4 @@
|
||||
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
sourcemap.json
|
||||
/*.rbxl.lock
|
||||
|
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 975 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 229 B |
|
Before Width: | Height: | Size: 584 B After Width: | Height: | Size: 584 B |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 273 B |
|
Before Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 241 B |
|
Before Width: | Height: | Size: 574 B |
|
Before Width: | Height: | Size: 607 B |
@@ -17,10 +17,6 @@ html {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #e7e7e7
|
||||
}
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
max-height:100%;
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -1 +0,0 @@
|
||||
print("Hello world, from client!")
|
||||
@@ -1 +0,0 @@
|
||||
print("Hello world, from server!")
|
||||
@@ -1,3 +0,0 @@
|
||||
return function()
|
||||
print("Hello, world!")
|
||||
end
|
||||
@@ -1,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).
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "{project_name}",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Plugin model files
|
||||
/{project_name}.rbxmx
|
||||
/{project_name}.rbxm
|
||||
|
||||
sourcemap.json
|
||||
@@ -1 +0,0 @@
|
||||
print("Hello world, from plugin!")
|
||||
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use librojo::cli::BuildCommand;
|
||||
use librojo::cli::{build, BuildCommand};
|
||||
|
||||
pub fn benchmark_small_place(c: &mut Criterion) {
|
||||
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| {
|
||||
b.iter_batched(
|
||||
|| place_setup(path),
|
||||
|(_dir, options)| options.run().unwrap(),
|
||||
|(_dir, options)| build(options).unwrap(),
|
||||
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) {
|
||||
let dir = tempdir().unwrap();
|
||||
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 {
|
||||
project: input,
|
||||
watch: false,
|
||||
plugin: None,
|
||||
output,
|
||||
};
|
||||
|
||||
|
||||
60
build.rs
@@ -7,7 +7,6 @@ use fs_err as fs;
|
||||
use fs_err::File;
|
||||
use maplit::hashmap;
|
||||
use memofs::VfsSnapshot;
|
||||
use semver::Version;
|
||||
|
||||
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||
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();
|
||||
|
||||
if file_name.starts_with(".git") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We can skip any TestEZ test files since they aren't necessary for
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -45,39 +40,38 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
|
||||
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
let plugin_dir = root_dir.join("plugin");
|
||||
let templates_dir = root_dir.join("assets").join("project-templates");
|
||||
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
||||
|
||||
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
||||
let plugin_version =
|
||||
Version::parse(fs::read_to_string(plugin_dir.join("Version.txt"))?.trim())?;
|
||||
let plugin_modules = plugin_root.join("modules");
|
||||
|
||||
assert_eq!(
|
||||
our_version, plugin_version,
|
||||
"plugin version does not match Cargo version"
|
||||
);
|
||||
|
||||
let template_snapshot = snapshot_from_fs_path(&templates_dir)?;
|
||||
|
||||
let plugin_snapshot = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?,
|
||||
"plugin" => VfsSnapshot::dir(hashmap! {
|
||||
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
|
||||
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
|
||||
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
|
||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
|
||||
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
||||
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
||||
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
|
||||
let snapshot = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
||||
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
||||
"http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
|
||||
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
||||
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
||||
"modules" => VfsSnapshot::dir(hashmap! {
|
||||
"roact" => VfsSnapshot::dir(hashmap! {
|
||||
"src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))?
|
||||
}),
|
||||
"promise" => VfsSnapshot::dir(hashmap! {
|
||||
"lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))?
|
||||
}),
|
||||
"t" => VfsSnapshot::dir(hashmap! {
|
||||
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
|
||||
}),
|
||||
"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 plugin_file = File::create(Path::new(&out_dir).join("plugin.bincode"))?;
|
||||
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||
let out_file = File::create(&out_path)?;
|
||||
|
||||
bincode::serialize_into(plugin_file, &plugin_snapshot)?;
|
||||
bincode::serialize_into(template_file, &template_snapshot)?;
|
||||
bincode::serialize_into(out_file, &snapshot)?;
|
||||
|
||||
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
|
||||
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
# memofs Changelog
|
||||
|
||||
## Unreleased Changes
|
||||
* Added `Vfs::canonicalize`. [#1201]
|
||||
|
||||
## 0.3.1 (2025-11-27)
|
||||
* Added `Vfs::exists`. [#1169]
|
||||
* Added `create_dir` and `create_dir_all` to allow creating directories. [#937]
|
||||
|
||||
[#1169]: https://github.com/rojo-rbx/rojo/pull/1169
|
||||
[#937]: https://github.com/rojo-rbx/rojo/pull/937
|
||||
|
||||
## 0.3.0 (2024-03-15)
|
||||
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
|
||||
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
|
||||
|
||||
[#830]: https://github.com/rojo-rbx/rojo/pull/830
|
||||
[#854]: https://github.com/rojo-rbx/rojo/pull/854
|
||||
|
||||
## 0.2.0 (2021-08-23)
|
||||
* Updated to `crossbeam-channel` 0.5.1.
|
||||
@@ -30,4 +15,4 @@
|
||||
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
|
||||
|
||||
## 0.1.0 (2020-03-10)
|
||||
* Initial release
|
||||
* Initial release
|
||||
@@ -1,12 +1,8 @@
|
||||
[package]
|
||||
name = "memofs"
|
||||
description = "Virtual filesystem with configurable backends."
|
||||
version = "0.3.1"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
"Ken Loeffler <kenloef@gmail.com>",
|
||||
]
|
||||
version = "0.2.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -15,10 +11,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
crossbeam-channel = "0.5.12"
|
||||
fs-err = "2.11.0"
|
||||
notify = "4.0.17"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10.1"
|
||||
crossbeam-channel = "0.5.1"
|
||||
fs-err = "2.3.0"
|
||||
notify = "4.0.15"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -50,12 +50,6 @@ impl InMemoryFs {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InMemoryFs {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InMemoryFsInner {
|
||||
entries: HashMap<PathBuf, Entry>,
|
||||
@@ -157,11 +151,6 @@ impl VfsBackend for InMemoryFs {
|
||||
)
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Path) -> io::Result<bool> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
Ok(inner.entries.contains_key(path))
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
@@ -181,21 +170,6 @@ impl VfsBackend for InMemoryFs {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
|
||||
}
|
||||
|
||||
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
let mut path_buf = path.to_path_buf();
|
||||
while let Some(parent) = path_buf.parent() {
|
||||
inner.load_snapshot(parent.to_path_buf(), VfsSnapshot::empty_dir())?;
|
||||
path_buf.pop();
|
||||
}
|
||||
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
@@ -232,33 +206,6 @@ impl VfsBackend for InMemoryFs {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We rely on Rojo to prepend cwd to any relative path before storing paths
|
||||
// in MemoFS. The current implementation will error if no prepended absolute path
|
||||
// is found. It really only normalizes paths within the provided path's context.
|
||||
// Example: "/Users/username/project/../other/file.txt" ->
|
||||
// "/Users/username/other/file.txt"
|
||||
// Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt"
|
||||
// This is not very robust. We should implement proper path normalization here or otherwise
|
||||
// warn if we are missing context and can not fully canonicalize the path correctly.
|
||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
std::path::Component::ParentDir => {
|
||||
normalized.pop();
|
||||
}
|
||||
std::path::Component::CurDir => {}
|
||||
_ => normalized.push(component),
|
||||
}
|
||||
}
|
||||
|
||||
let inner = self.inner.lock().unwrap();
|
||||
match inner.entries.get(&normalized) {
|
||||
Some(_) => Ok(normalized),
|
||||
None => not_found(&normalized),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
@@ -275,17 +222,23 @@ impl VfsBackend for InMemoryFs {
|
||||
}
|
||||
|
||||
fn must_be_file<T>(path: &Path) -> io::Result<T> {
|
||||
Err(io::Error::other(format!(
|
||||
"path {} was a directory, but must be a file",
|
||||
path.display()
|
||||
)))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"path {} was a directory, but must be a file",
|
||||
path.display()
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn must_be_dir<T>(path: &Path) -> io::Result<T> {
|
||||
Err(io::Error::other(format!(
|
||||
"path {} was a file, but must be a directory",
|
||||
path.display()
|
||||
)))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"path {} was a file, but must be a directory",
|
||||
path.display()
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn not_found<T>(path: &Path) -> io::Result<T> {
|
||||
|
||||
@@ -22,9 +22,9 @@ mod noop_backend;
|
||||
mod snapshot;
|
||||
mod std_backend;
|
||||
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::{io, str};
|
||||
|
||||
pub use in_memory_fs::InMemoryFs;
|
||||
pub use noop_backend::NoopBackend;
|
||||
@@ -70,14 +70,10 @@ impl<T> IoResultExt<T> for io::Result<T> {
|
||||
pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
|
||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
|
||||
fn exists(&mut self, path: &Path) -> io::Result<bool>;
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
|
||||
fn create_dir(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn create_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf>;
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
||||
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
||||
@@ -159,29 +155,6 @@ impl VfsInner {
|
||||
Ok(Arc::new(contents))
|
||||
}
|
||||
|
||||
fn read_to_string<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<String>> {
|
||||
let path = path.as_ref();
|
||||
let contents = self.backend.read(path)?;
|
||||
|
||||
if self.watch_enabled {
|
||||
self.backend.watch(path)?;
|
||||
}
|
||||
|
||||
let contents_str = str::from_utf8(&contents).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("File was not valid UTF-8: {}", path.display()),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Arc::new(contents_str.into()))
|
||||
}
|
||||
|
||||
fn exists<P: AsRef<Path>>(&mut self, path: P) -> io::Result<bool> {
|
||||
let path = path.as_ref();
|
||||
self.backend.exists(path)
|
||||
}
|
||||
|
||||
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let contents = contents.as_ref();
|
||||
@@ -199,16 +172,6 @@ impl VfsInner {
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.backend.create_dir(path)
|
||||
}
|
||||
|
||||
fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.backend.create_dir_all(path)
|
||||
}
|
||||
|
||||
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let _ = self.backend.unwatch(path);
|
||||
@@ -226,18 +189,16 @@ impl VfsInner {
|
||||
self.backend.metadata(path)
|
||||
}
|
||||
|
||||
fn canonicalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
self.backend.canonicalize(path)
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.backend.event_receiver()
|
||||
}
|
||||
|
||||
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
||||
if let VfsEvent::Remove(path) = event {
|
||||
let _ = self.backend.unwatch(path);
|
||||
match event {
|
||||
VfsEvent::Remove(path) => {
|
||||
let _ = self.backend.unwatch(&path);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -300,33 +261,6 @@ impl Vfs {
|
||||
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.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
||||
@@ -350,42 +284,6 @@ impl Vfs {
|
||||
self.inner.lock().unwrap().read_dir(path)
|
||||
}
|
||||
|
||||
/// Return whether the given path exists.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::exists`][std::fs::exists].
|
||||
///
|
||||
/// [std::fs::exists]: https://doc.rust-lang.org/stable/std/fs/fn.exists.html
|
||||
#[inline]
|
||||
pub fn exists<P: AsRef<Path>>(&self, path: P) -> io::Result<bool> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().exists(path)
|
||||
}
|
||||
|
||||
/// Creates a directory at the provided location.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
|
||||
/// Similiar to that function, this function will fail if the parent of the
|
||||
/// path does not exist.
|
||||
///
|
||||
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
|
||||
#[inline]
|
||||
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().create_dir(path)
|
||||
}
|
||||
|
||||
/// Creates a directory at the provided location, recursively creating
|
||||
/// all parent components if they are missing.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
|
||||
///
|
||||
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
|
||||
#[inline]
|
||||
pub fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().create_dir_all(path)
|
||||
}
|
||||
|
||||
/// Remove a file.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||
@@ -419,19 +317,6 @@ impl Vfs {
|
||||
self.inner.lock().unwrap().metadata(path)
|
||||
}
|
||||
|
||||
/// Normalize a path via the underlying backend.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are
|
||||
/// resolved against the backend's current working directory (if applicable) and errors are
|
||||
/// surfaced directly from the backend.
|
||||
///
|
||||
/// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
|
||||
#[inline]
|
||||
pub fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().canonicalize(path)
|
||||
}
|
||||
|
||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||
#[inline]
|
||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
@@ -501,31 +386,6 @@ impl VfsLock<'_> {
|
||||
self.inner.read_dir(path)
|
||||
}
|
||||
|
||||
/// Creates a directory at the provided location.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
|
||||
/// Similiar to that function, this function will fail if the parent of the
|
||||
/// path does not exist.
|
||||
///
|
||||
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
|
||||
#[inline]
|
||||
pub fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.create_dir(path)
|
||||
}
|
||||
|
||||
/// Creates a directory at the provided location, recursively creating
|
||||
/// all parent components if they are missing.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
|
||||
///
|
||||
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
|
||||
#[inline]
|
||||
pub fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.create_dir_all(path)
|
||||
}
|
||||
|
||||
/// Remove a file.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||
@@ -559,13 +419,6 @@ impl VfsLock<'_> {
|
||||
self.inner.metadata(path)
|
||||
}
|
||||
|
||||
/// Normalize a path via the underlying backend.
|
||||
#[inline]
|
||||
pub fn normalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
self.inner.canonicalize(path)
|
||||
}
|
||||
|
||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||
#[inline]
|
||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
@@ -578,83 +431,3 @@ impl VfsLock<'_> {
|
||||
self.inner.commit_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{InMemoryFs, StdBackend, Vfs, VfsSnapshot};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// https://github.com/rojo-rbx/rojo/issues/899
|
||||
#[test]
|
||||
fn read_to_string_lf_normalized_keeps_trailing_newline() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("test", VfsSnapshot::file("bar\r\nfoo\r\n\r\n"))
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
assert_eq!(
|
||||
vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
|
||||
"bar\nfoo\n\n"
|
||||
);
|
||||
}
|
||||
|
||||
/// https://github.com/rojo-rbx/rojo/issues/1200
|
||||
#[test]
|
||||
fn canonicalize_in_memory_success() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
||||
|
||||
imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string()))
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
assert_eq!(
|
||||
vfs.canonicalize("/test/nested/../file.txt").unwrap(),
|
||||
PathBuf::from("/test/file.txt")
|
||||
);
|
||||
assert_eq!(
|
||||
vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap())
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
contents.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_in_memory_missing_errors() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
let err = vfs.canonicalize("test").unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_std_backend_success() {
|
||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join("file.txt");
|
||||
fs_err::write(&file_path, contents.to_string()).unwrap();
|
||||
|
||||
let vfs = Vfs::new(StdBackend::new());
|
||||
let canonicalized = vfs.canonicalize(&file_path).unwrap();
|
||||
assert_eq!(canonicalized, file_path.canonicalize().unwrap());
|
||||
assert_eq!(
|
||||
vfs.read_to_string(&canonicalized).unwrap().to_string(),
|
||||
contents.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_std_backend_missing_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join("test");
|
||||
|
||||
let vfs = Vfs::new(StdBackend::new());
|
||||
let err = vfs.canonicalize(&file_path).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||
|
||||
@@ -15,43 +15,45 @@ impl NoopBackend {
|
||||
|
||||
impl VfsBackend for NoopBackend {
|
||||
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn exists(&mut self, _path: &Path) -> io::Result<bool> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn create_dir(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn canonicalize(&mut self, _path: &Path) -> io::Result<PathBuf> {
|
||||
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> {
|
||||
@@ -59,16 +61,16 @@ impl VfsBackend for NoopBackend {
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NoopBackend {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashSet, io};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
@@ -13,7 +13,6 @@ use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||
pub struct StdBackend {
|
||||
watcher: RecommendedWatcher,
|
||||
watcher_receiver: Receiver<VfsEvent>,
|
||||
watches: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl StdBackend {
|
||||
@@ -49,7 +48,6 @@ impl StdBackend {
|
||||
Self {
|
||||
watcher,
|
||||
watcher_receiver: rx,
|
||||
watches: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,10 +61,6 @@ impl VfsBackend for StdBackend {
|
||||
fs_err::write(path, data)
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Path) -> io::Result<bool> {
|
||||
std::fs::exists(path)
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
|
||||
let mut entries = entries?;
|
||||
@@ -82,14 +76,6 @@ impl VfsBackend for StdBackend {
|
||||
})
|
||||
}
|
||||
|
||||
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
|
||||
fs_err::create_dir(path)
|
||||
}
|
||||
|
||||
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||
fs_err::create_dir_all(path)
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||
fs_err::remove_file(path)
|
||||
}
|
||||
@@ -106,37 +92,19 @@ impl VfsBackend for StdBackend {
|
||||
})
|
||||
}
|
||||
|
||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
||||
fs_err::canonicalize(path)
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.watcher_receiver.clone()
|
||||
}
|
||||
|
||||
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
||||
if self.watches.contains(path)
|
||||
|| path
|
||||
.ancestors()
|
||||
.any(|ancestor| self.watches.contains(ancestor))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
self.watches.insert(path.to_path_buf());
|
||||
self.watcher
|
||||
.watch(path, RecursiveMode::Recursive)
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
self.watcher
|
||||
.watch(path, RecursiveMode::NonRecursive)
|
||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
||||
self.watches.remove(path);
|
||||
self.watcher.unwatch(path).map_err(io::Error::other)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StdBackend {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
self.watcher
|
||||
.unwatch(path)
|
||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.197"
|
||||
serde_yaml = "0.8.26"
|
||||
serde = "1.0.99"
|
||||
serde_yaml = "0.8.9"
|
||||
|
||||
@@ -5,13 +5,19 @@ use serde::Serialize;
|
||||
/// Enables redacting any value that serializes as a string.
|
||||
///
|
||||
/// Used for transforming Rojo instance IDs into something deterministic.
|
||||
#[derive(Default)]
|
||||
pub struct RedactionMap {
|
||||
ids: HashMap<String, usize>,
|
||||
last_id: usize,
|
||||
}
|
||||
|
||||
impl RedactionMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ids: HashMap::new(),
|
||||
last_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_redacted_value(&self, id: impl ToString) -> Option<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) {
|
||||
let last_id = &mut self.last_id;
|
||||
|
||||
|
||||
16
crates/rojo-project/Cargo.toml
Normal 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"
|
||||
4
crates/rojo-project/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# rojo-project
|
||||
Project file format crate for [Rojo].
|
||||
|
||||
[Rojo]: https://rojo.space
|
||||
@@ -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 globset::{Glob as InnerGlob, GlobMatcher};
|
||||
@@ -8,6 +5,8 @@ use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
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)]
|
||||
pub struct Glob {
|
||||
inner: InnerGlob,
|
||||
@@ -43,8 +42,8 @@ impl Serialize for Glob {
|
||||
|
||||
impl<'de> Deserialize<'de> for Glob {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let glob = String::deserialize(deserializer)?;
|
||||
let glob = <&str as Deserialize>::deserialize(deserializer)?;
|
||||
|
||||
Glob::new(&glob).map_err(D::Error::custom)
|
||||
Glob::new(glob).map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
7
crates/rojo-project/src/lib.rs
Normal 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};
|
||||
21
crates/rojo-project/src/path_serializer.rs
Normal 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)
|
||||
}
|
||||
363
crates/rojo-project/src/project.rs
Normal 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"}"#);
|
||||
}
|
||||
}
|
||||
294
crates/rojo-project/src/resolution.rs
Normal 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
@@ -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" }
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
7.7.0-rc.1
|
||||
33
plugin/default.project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
local function defaultTableDebug(buffer, input)
|
||||
buffer:writeRaw("{")
|
||||
|
||||
for key, value in input do
|
||||
for key, value in pairs(input) do
|
||||
buffer:write("[{:?}] = {:?}", key, value)
|
||||
|
||||
if next(input, key) ~= nil then
|
||||
@@ -50,7 +50,7 @@ local function defaultTableDebugExtended(buffer, input)
|
||||
buffer:writeLineRaw("{")
|
||||
buffer:indent()
|
||||
|
||||
for key, value in input do
|
||||
for key, value in pairs(input) do
|
||||
buffer:writeLine("[{:?}] = {:#?},", key, value)
|
||||
end
|
||||
|
||||
@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
|
||||
elseif valueType == "table" then
|
||||
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
|
||||
-- Rust's 'Debug' trait.
|
||||
|
||||
@@ -242,4 +242,4 @@ return {
|
||||
debugOutputBuffer = debugOutputBuffer,
|
||||
fmt = fmt,
|
||||
debugify = debugify,
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ Error.__index = Error
|
||||
|
||||
Error.Kind = {
|
||||
HttpNotEnabled = {
|
||||
message = "Rojo requires HTTP access, which is not enabled.\n"
|
||||
.. "Check your game settings, located in the 'Home' tab of Studio.",
|
||||
message = "Rojo requires HTTP access, which is not enabled.\n" ..
|
||||
"Check your game settings, located in the 'Home' tab of Studio.",
|
||||
},
|
||||
ConnectFailed = {
|
||||
message = "Couldn't connect to the Rojo server.\n"
|
||||
.. "Make sure the server is running — use 'rojo serve' to run it!",
|
||||
message = "Couldn't connect to the Rojo server.\n" ..
|
||||
"Make sure the server is running — use 'rojo serve' to run it!",
|
||||
},
|
||||
Timeout = {
|
||||
message = "HTTP request timed out.",
|
||||
@@ -63,13 +63,4 @@ function Error.fromRobloxErrorString(message)
|
||||
return Error.new(Error.Kind.Unknown, message)
|
||||
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
|
||||
|
||||
@@ -31,4 +31,4 @@ function Response:json()
|
||||
return HttpService:JSONDecode(self.body)
|
||||
end
|
||||
|
||||
return Response
|
||||
return Response
|
||||
@@ -30,13 +30,8 @@ local function performRequest(requestParams)
|
||||
end)
|
||||
|
||||
if success then
|
||||
Log.trace("Request {} success, response {:#?}", requestId, response)
|
||||
local httpResponse = HttpResponse.fromRobloxResponse(response)
|
||||
if httpResponse:isSuccess() then
|
||||
resolve(httpResponse)
|
||||
else
|
||||
reject(HttpError.fromResponse(httpResponse))
|
||||
end
|
||||
Log.trace("Request {} success, status code {}", requestId, response.StatusCode)
|
||||
resolve(HttpResponse.fromRobloxResponse(response))
|
||||
else
|
||||
Log.trace("Request {} failure: {:?}", requestId, response)
|
||||
reject(HttpError.fromRobloxErrorString(response))
|
||||
@@ -68,4 +63,4 @@ function Http.jsonDecode(source)
|
||||
return HttpService:JSONDecode(source)
|
||||
end
|
||||
|
||||
return Http
|
||||
return Http
|
||||
@@ -2,4 +2,4 @@ return function()
|
||||
it("should load", function()
|
||||
require(script.Parent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -57,4 +57,4 @@ function Log.error(template, ...)
|
||||
error(Fmt.fmt(template, ...))
|
||||
end
|
||||
|
||||
return Log
|
||||
return Log
|
||||
@@ -2,4 +2,4 @@ return function()
|
||||
it("should load", function()
|
||||
require(script.Parent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
1
plugin/modules/flipper
Submodule
1
plugin/modules/promise
Submodule
1
plugin/modules/roact
Submodule
1
plugin/modules/t
Submodule
1
plugin/modules/testez
Submodule
@@ -20,54 +20,11 @@ local function serializeFloat(value)
|
||||
return value
|
||||
end
|
||||
|
||||
local ALL_AXES = { "X", "Y", "Z" }
|
||||
local ALL_FACES = { "Right", "Top", "Back", "Left", "Bottom", "Front" }
|
||||
|
||||
local EncodedValue = {}
|
||||
local ALL_AXES = {"X", "Y", "Z"}
|
||||
local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
|
||||
|
||||
local types
|
||||
types = {
|
||||
Attributes = {
|
||||
fromPod = function(pod)
|
||||
local output = {}
|
||||
|
||||
for key, value in pairs(pod) do
|
||||
local ok, result = EncodedValue.decode(value)
|
||||
|
||||
if ok then
|
||||
output[key] = result
|
||||
else
|
||||
local warning = ("Could not decode attribute value of type %q: %s"):format(
|
||||
typeof(value),
|
||||
tostring(result)
|
||||
)
|
||||
warn(warning)
|
||||
end
|
||||
end
|
||||
|
||||
return output
|
||||
end,
|
||||
toPod = function(roblox)
|
||||
local output = {}
|
||||
|
||||
for key, value in pairs(roblox) do
|
||||
local ok, result = EncodedValue.encodeNaive(value)
|
||||
|
||||
if ok then
|
||||
output[key] = result
|
||||
else
|
||||
local warning = ("Could not encode attribute value of type %q: %s"):format(
|
||||
typeof(value),
|
||||
tostring(result)
|
||||
)
|
||||
warn(warning)
|
||||
end
|
||||
end
|
||||
|
||||
return output
|
||||
end,
|
||||
},
|
||||
|
||||
Axes = {
|
||||
fromPod = function(pod)
|
||||
local axes = {}
|
||||
@@ -117,7 +74,6 @@ types = {
|
||||
local pos = pod.position
|
||||
local orient = pod.orientation
|
||||
|
||||
--stylua: ignore
|
||||
return CFrame.new(
|
||||
pos[1], pos[2], pos[3],
|
||||
orient[1][1], orient[1][2], orient[1][3],
|
||||
@@ -127,14 +83,17 @@ types = {
|
||||
end,
|
||||
|
||||
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 {
|
||||
position = { x, y, z },
|
||||
position = {x, y, z},
|
||||
orientation = {
|
||||
{ r00, r01, r02 },
|
||||
{ r10, r11, r12 },
|
||||
{ r20, r21, r22 },
|
||||
{r00, r01, r02},
|
||||
{r10, r11, r12},
|
||||
{r20, r21, r22},
|
||||
},
|
||||
}
|
||||
end,
|
||||
@@ -144,7 +103,7 @@ types = {
|
||||
fromPod = unpackDecoder(Color3.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.r, roblox.g, roblox.b }
|
||||
return {roblox.r, roblox.g, roblox.b}
|
||||
end,
|
||||
},
|
||||
|
||||
@@ -165,7 +124,10 @@ types = {
|
||||
local keypoints = {}
|
||||
|
||||
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
|
||||
|
||||
return ColorSequence.new(keypoints)
|
||||
@@ -188,38 +150,6 @@ types = {
|
||||
},
|
||||
|
||||
Content = {
|
||||
fromPod = function(pod): Content
|
||||
if type(pod) == "string" then
|
||||
if pod == "None" then
|
||||
return Content.none
|
||||
else
|
||||
error(`unexpected Content value '{pod}'`)
|
||||
end
|
||||
else
|
||||
local ty, value = next(pod)
|
||||
if ty == "Uri" then
|
||||
return Content.fromUri(value)
|
||||
elseif ty == "Object" then
|
||||
error("Object deserializing is not currently implemented")
|
||||
else
|
||||
error(`Unknown Content type '{ty}' (could not deserialize)`)
|
||||
end
|
||||
end
|
||||
end,
|
||||
toPod = function(roblox: Content)
|
||||
if roblox.SourceType == Enum.ContentSourceType.None then
|
||||
return "None"
|
||||
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
|
||||
return { Uri = roblox.Uri }
|
||||
elseif roblox.SourceType == Enum.ContentSourceType.Object then
|
||||
error("Object serializing is not currently implemented")
|
||||
else
|
||||
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
|
||||
end
|
||||
end,
|
||||
},
|
||||
|
||||
ContentId = {
|
||||
fromPod = identity,
|
||||
toPod = identity,
|
||||
},
|
||||
@@ -237,19 +167,6 @@ types = {
|
||||
end,
|
||||
},
|
||||
|
||||
EnumItem = {
|
||||
fromPod = function(pod)
|
||||
return Enum[pod.type]:FromValue(pod.value)
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
return {
|
||||
type = tostring(roblox.EnumType),
|
||||
value = roblox.Value,
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
Faces = {
|
||||
fromPod = function(pod)
|
||||
local faces = {}
|
||||
@@ -284,23 +201,6 @@ types = {
|
||||
toPod = serializeFloat,
|
||||
},
|
||||
|
||||
Font = {
|
||||
fromPod = function(pod)
|
||||
return Font.new(
|
||||
pod.family,
|
||||
if pod.weight ~= nil then Enum.FontWeight[pod.weight] else nil,
|
||||
if pod.style ~= nil then Enum.FontStyle[pod.style] else nil
|
||||
)
|
||||
end,
|
||||
toPod = function(roblox)
|
||||
return {
|
||||
family = roblox.Family,
|
||||
weight = roblox.Weight.Name,
|
||||
style = roblox.Style.Name,
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
Int32 = {
|
||||
fromPod = identity,
|
||||
toPod = identity,
|
||||
@@ -311,32 +211,11 @@ types = {
|
||||
toPod = identity,
|
||||
},
|
||||
|
||||
MaterialColors = {
|
||||
fromPod = function(pod: { [string]: { number } })
|
||||
local real = {}
|
||||
for name, color in pod do
|
||||
real[Enum.Material[name]] = Color3.fromRGB(color[1], color[2], color[3])
|
||||
end
|
||||
return real
|
||||
end,
|
||||
toPod = function(roblox: { [Enum.Material]: Color3 })
|
||||
local pod = {}
|
||||
for material, color in roblox do
|
||||
pod[material.Name] = {
|
||||
math.round(math.clamp(color.R, 0, 1) * 255),
|
||||
math.round(math.clamp(color.G, 0, 1) * 255),
|
||||
math.round(math.clamp(color.B, 0, 1) * 255),
|
||||
}
|
||||
end
|
||||
return pod
|
||||
end,
|
||||
},
|
||||
|
||||
NumberRange = {
|
||||
fromPod = unpackDecoder(NumberRange.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.Min, roblox.Max }
|
||||
return {roblox.Min, roblox.Max}
|
||||
end,
|
||||
},
|
||||
|
||||
@@ -345,12 +224,11 @@ types = {
|
||||
local keypoints = {}
|
||||
|
||||
for index, keypoint in ipairs(pod.keypoints) do
|
||||
-- TODO: Add a test for NaN or Infinity values and envelopes
|
||||
-- Right now it isn't possible because it'd fail the roundtrip.
|
||||
-- It's more important that it works right now, though.
|
||||
local value = keypoint.value or 0
|
||||
local envelope = keypoint.envelope or 0
|
||||
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
|
||||
keypoints[index] = NumberSequenceKeypoint.new(
|
||||
keypoint.time,
|
||||
keypoint.value,
|
||||
keypoint.envelope
|
||||
)
|
||||
end
|
||||
|
||||
return NumberSequence.new(keypoints)
|
||||
@@ -378,26 +256,13 @@ types = {
|
||||
if pod == "Default" then
|
||||
return nil
|
||||
else
|
||||
-- Passing `nil` instead of not passing anything gives
|
||||
-- different results, so we have to branch here.
|
||||
if pod.acousticAbsorption then
|
||||
return (PhysicalProperties.new :: any)(
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight,
|
||||
pod.acousticAbsorption
|
||||
)
|
||||
else
|
||||
return PhysicalProperties.new(
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight
|
||||
)
|
||||
end
|
||||
return PhysicalProperties.new(
|
||||
pod.density,
|
||||
pod.friction,
|
||||
pod.elasticity,
|
||||
pod.frictionWeight,
|
||||
pod.elasticityWeight
|
||||
)
|
||||
end
|
||||
end,
|
||||
|
||||
@@ -411,7 +276,6 @@ types = {
|
||||
elasticity = roblox.Elasticity,
|
||||
frictionWeight = roblox.FrictionWeight,
|
||||
elasticityWeight = roblox.ElasticityWeight,
|
||||
acousticAbsorption = roblox.AcousticAbsorption,
|
||||
}
|
||||
end
|
||||
end,
|
||||
@@ -419,7 +283,10 @@ types = {
|
||||
|
||||
Ray = {
|
||||
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,
|
||||
|
||||
toPod = function(roblox)
|
||||
@@ -432,7 +299,10 @@ types = {
|
||||
|
||||
Rect = {
|
||||
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,
|
||||
|
||||
toPod = function(roblox)
|
||||
@@ -444,28 +314,31 @@ types = {
|
||||
},
|
||||
|
||||
Ref = {
|
||||
fromPod = function(_)
|
||||
fromPod = function(_pod)
|
||||
error("Ref cannot be decoded on its own")
|
||||
end,
|
||||
|
||||
toPod = function(_)
|
||||
toPod = function(_roblox)
|
||||
error("Ref can not be encoded on its own")
|
||||
end,
|
||||
},
|
||||
|
||||
Region3 = {
|
||||
fromPod = function(_)
|
||||
fromPod = function(pod)
|
||||
error("Region3 is not implemented")
|
||||
end,
|
||||
|
||||
toPod = function(_)
|
||||
toPod = function(roblox)
|
||||
error("Region3 is not implemented")
|
||||
end,
|
||||
},
|
||||
|
||||
Region3int16 = {
|
||||
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,
|
||||
|
||||
toPod = function(roblox)
|
||||
@@ -477,11 +350,11 @@ types = {
|
||||
},
|
||||
|
||||
SharedString = {
|
||||
fromPod = function(_pod)
|
||||
fromPod = function(pod)
|
||||
error("SharedString is not supported")
|
||||
end,
|
||||
|
||||
toPod = function(_roblox)
|
||||
toPod = function(roblox)
|
||||
error("SharedString is not supported")
|
||||
end,
|
||||
},
|
||||
@@ -495,13 +368,16 @@ types = {
|
||||
fromPod = unpackDecoder(UDim.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.Scale, roblox.Offset }
|
||||
return {roblox.Scale, roblox.Offset}
|
||||
end,
|
||||
},
|
||||
|
||||
UDim2 = {
|
||||
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,
|
||||
|
||||
toPod = function(roblox)
|
||||
@@ -532,7 +408,7 @@ types = {
|
||||
fromPod = unpackDecoder(Vector2int16.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.X, roblox.Y }
|
||||
return {roblox.X, roblox.Y}
|
||||
end,
|
||||
},
|
||||
|
||||
@@ -552,37 +428,16 @@ types = {
|
||||
fromPod = unpackDecoder(Vector3int16.new),
|
||||
|
||||
toPod = function(roblox)
|
||||
return { roblox.X, roblox.Y, roblox.Z }
|
||||
return {roblox.X, roblox.Y, roblox.Z}
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
types.OptionalCFrame = {
|
||||
fromPod = function(pod)
|
||||
if pod == nil then
|
||||
return nil
|
||||
else
|
||||
return types.CFrame.fromPod(pod)
|
||||
end
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
if roblox == nil then
|
||||
return nil
|
||||
else
|
||||
return types.CFrame.toPod(roblox)
|
||||
end
|
||||
end,
|
||||
}
|
||||
local EncodedValue = {}
|
||||
|
||||
function EncodedValue.decode(encodedValue)
|
||||
local ty, value = next(encodedValue)
|
||||
|
||||
if ty == nil then
|
||||
-- If the encoded pair is empty, assume it is an unoccupied optional value
|
||||
return true, nil
|
||||
end
|
||||
|
||||
local typeImpl = types[ty]
|
||||
if typeImpl == nil then
|
||||
return false, "Couldn't decode value " .. tostring(ty)
|
||||
@@ -604,19 +459,4 @@ function EncodedValue.encode(rbxValue, propertyType)
|
||||
}
|
||||
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
|
||||
|
||||
72
plugin/rbx_dom_lua/EncodedValue.spec.lua
Normal 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
|
||||
@@ -5,7 +5,6 @@ Error.Kind = {
|
||||
UnknownProperty = "UnknownProperty",
|
||||
PropertyNotReadable = "PropertyNotReadable",
|
||||
PropertyNotWritable = "PropertyNotWritable",
|
||||
CannotParseBinaryString = "CannotParseBinaryString",
|
||||
Roblox = "Roblox",
|
||||
}
|
||||
|
||||
@@ -26,4 +25,4 @@ function Error:__tostring()
|
||||
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
|
||||
end
|
||||
|
||||
return Error
|
||||
return Error
|
||||
@@ -53,11 +53,6 @@ function PropertyDescriptor:read(instance)
|
||||
end
|
||||
|
||||
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]
|
||||
|
||||
return interface.read(instance, self.name)
|
||||
@@ -84,11 +79,6 @@ function PropertyDescriptor:write(instance, value)
|
||||
end
|
||||
|
||||
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]
|
||||
|
||||
return interface.write(instance, self.name, value)
|
||||
|
||||
@@ -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": {
|
||||
"value": {
|
||||
"Axes": [
|
||||
@@ -176,23 +101,9 @@
|
||||
},
|
||||
"ty": "ColorSequence"
|
||||
},
|
||||
"ContentId": {
|
||||
"Content": {
|
||||
"value": {
|
||||
"ContentId": "rbxassetid://12345"
|
||||
},
|
||||
"ty": "ContentId"
|
||||
},
|
||||
"Content_None": {
|
||||
"value": {
|
||||
"Content": "None"
|
||||
},
|
||||
"ty": "Content"
|
||||
},
|
||||
"Content_Uri": {
|
||||
"value": {
|
||||
"Content": {
|
||||
"Uri": "rbxasset://abc/123.rojo"
|
||||
}
|
||||
"Content": "rbxassetid://12345"
|
||||
},
|
||||
"ty": "Content"
|
||||
},
|
||||
@@ -202,15 +113,6 @@
|
||||
},
|
||||
"ty": "Enum"
|
||||
},
|
||||
"EnumItem": {
|
||||
"value": {
|
||||
"EnumItem": {
|
||||
"type": "Material",
|
||||
"value": 256
|
||||
}
|
||||
},
|
||||
"ty": "EnumItem"
|
||||
},
|
||||
"Faces": {
|
||||
"value": {
|
||||
"Faces": [
|
||||
@@ -236,17 +138,6 @@
|
||||
},
|
||||
"ty": "Float64"
|
||||
},
|
||||
"Font": {
|
||||
"value": {
|
||||
"Font": {
|
||||
"family": "rbxasset://fonts/families/SourceSansPro.json",
|
||||
"weight": "Regular",
|
||||
"style": "Normal",
|
||||
"cachedFaceId": null
|
||||
}
|
||||
},
|
||||
"ty": "Font"
|
||||
},
|
||||
"Int32": {
|
||||
"value": {
|
||||
"Int32": 6014
|
||||
@@ -259,118 +150,6 @@
|
||||
},
|
||||
"ty": "Int64"
|
||||
},
|
||||
"MaterialColors": {
|
||||
"value": {
|
||||
"MaterialColors": {
|
||||
"Grass": [
|
||||
106,
|
||||
127,
|
||||
63
|
||||
],
|
||||
"Slate": [
|
||||
63,
|
||||
127,
|
||||
107
|
||||
],
|
||||
"Concrete": [
|
||||
127,
|
||||
102,
|
||||
63
|
||||
],
|
||||
"Brick": [
|
||||
138,
|
||||
86,
|
||||
62
|
||||
],
|
||||
"Sand": [
|
||||
143,
|
||||
126,
|
||||
95
|
||||
],
|
||||
"WoodPlanks": [
|
||||
139,
|
||||
109,
|
||||
79
|
||||
],
|
||||
"Rock": [
|
||||
102,
|
||||
108,
|
||||
111
|
||||
],
|
||||
"Glacier": [
|
||||
101,
|
||||
176,
|
||||
234
|
||||
],
|
||||
"Snow": [
|
||||
195,
|
||||
199,
|
||||
218
|
||||
],
|
||||
"Sandstone": [
|
||||
137,
|
||||
90,
|
||||
71
|
||||
],
|
||||
"Mud": [
|
||||
58,
|
||||
46,
|
||||
36
|
||||
],
|
||||
"Basalt": [
|
||||
30,
|
||||
30,
|
||||
37
|
||||
],
|
||||
"Ground": [
|
||||
102,
|
||||
92,
|
||||
59
|
||||
],
|
||||
"CrackedLava": [
|
||||
232,
|
||||
156,
|
||||
74
|
||||
],
|
||||
"Asphalt": [
|
||||
115,
|
||||
123,
|
||||
107
|
||||
],
|
||||
"Cobblestone": [
|
||||
132,
|
||||
123,
|
||||
90
|
||||
],
|
||||
"Ice": [
|
||||
129,
|
||||
194,
|
||||
224
|
||||
],
|
||||
"LeafyGrass": [
|
||||
115,
|
||||
132,
|
||||
74
|
||||
],
|
||||
"Salt": [
|
||||
198,
|
||||
189,
|
||||
181
|
||||
],
|
||||
"Limestone": [
|
||||
206,
|
||||
173,
|
||||
148
|
||||
],
|
||||
"Pavement": [
|
||||
148,
|
||||
148,
|
||||
140
|
||||
]
|
||||
}
|
||||
},
|
||||
"ty": "MaterialColors"
|
||||
},
|
||||
"NumberRange": {
|
||||
"value": {
|
||||
"NumberRange": [
|
||||
@@ -399,41 +178,6 @@
|
||||
},
|
||||
"ty": "NumberSequence"
|
||||
},
|
||||
"OptionalCFrame-None": {
|
||||
"value": {
|
||||
"OptionalCFrame": null
|
||||
},
|
||||
"ty": "OptionalCFrame"
|
||||
},
|
||||
"OptionalCFrame-Some": {
|
||||
"value": {
|
||||
"OptionalCFrame": {
|
||||
"position": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"orientation": [
|
||||
[
|
||||
1.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
1.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"ty": "OptionalCFrame"
|
||||
},
|
||||
"PhysicalProperties-Custom": {
|
||||
"value": {
|
||||
"PhysicalProperties": {
|
||||
@@ -441,8 +185,7 @@
|
||||
"friction": 1.0,
|
||||
"elasticity": 0.0,
|
||||
"frictionWeight": 50.0,
|
||||
"elasticityWeight": 25.0,
|
||||
"acousticAbsorption": 0.15625
|
||||
"elasticityWeight": 25.0
|
||||
}
|
||||
},
|
||||
"ty": "PhysicalProperties"
|
||||
|
||||
@@ -1,10 +1,139 @@
|
||||
local EncodingService = game:GetService("EncodingService")
|
||||
-- Thanks to Tiffany352 for this base64 implementation!
|
||||
|
||||
local floor = math.floor
|
||||
local char = string.char
|
||||
|
||||
local function encodeBase64(str)
|
||||
local out = {}
|
||||
local nOut = 0
|
||||
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
local strLen = #str
|
||||
|
||||
-- 3 octets become 4 hextets
|
||||
for i = 1, strLen - 2, 3 do
|
||||
local b1, b2, b3 = str:byte(i, i + 3)
|
||||
local word = b3 + b2 * 256 + b1 * 256 * 256
|
||||
|
||||
local h4 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h3 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h2 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h1 = word % 64 + 1
|
||||
|
||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||
out[nOut + 4] = alphabet:sub(h4, h4)
|
||||
nOut = nOut + 4
|
||||
end
|
||||
|
||||
local remainder = strLen % 3
|
||||
|
||||
if remainder == 2 then
|
||||
-- 16 input bits -> 3 hextets (2 full, 1 partial)
|
||||
local b1, b2 = str:byte(-2, -1)
|
||||
-- partial is 4 bits long, leaving 2 bits of zero padding ->
|
||||
-- offset = 4
|
||||
local word = b2 * 4 + b1 * 4 * 256
|
||||
|
||||
local h3 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h2 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h1 = word % 64 + 1
|
||||
|
||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||
out[nOut + 4] = "="
|
||||
elseif remainder == 1 then
|
||||
-- 8 input bits -> 2 hextets (2 full, 1 partial)
|
||||
local b1 = str:byte(-1, -1)
|
||||
-- partial is 2 bits long, leaving 4 bits of zero padding ->
|
||||
-- offset = 16
|
||||
local word = b1 * 16
|
||||
|
||||
local h2 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h1 = word % 64 + 1
|
||||
|
||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||
out[nOut + 3] = "="
|
||||
out[nOut + 4] = "="
|
||||
end
|
||||
-- if the remainder is 0, then no work is needed
|
||||
|
||||
return table.concat(out, "")
|
||||
end
|
||||
|
||||
local function decodeBase64(str)
|
||||
local out = {}
|
||||
local nOut = 0
|
||||
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
local strLen = #str
|
||||
local acc = 0
|
||||
local nAcc = 0
|
||||
|
||||
local alphabetLut = {}
|
||||
for i = 1, #alphabet do
|
||||
alphabetLut[alphabet:sub(i, i)] = i - 1
|
||||
end
|
||||
|
||||
-- 4 hextets become 3 octets
|
||||
for i = 1, strLen do
|
||||
local ch = str:sub(i, i)
|
||||
local byte = alphabetLut[ch]
|
||||
if byte then
|
||||
acc = acc * 64 + byte
|
||||
nAcc = nAcc + 1
|
||||
end
|
||||
|
||||
if nAcc == 4 then
|
||||
local b3 = acc % 256
|
||||
acc = floor(acc / 256)
|
||||
local b2 = acc % 256
|
||||
acc = floor(acc / 256)
|
||||
local b1 = acc % 256
|
||||
|
||||
out[nOut + 1] = char(b1)
|
||||
out[nOut + 2] = char(b2)
|
||||
out[nOut + 3] = char(b3)
|
||||
nOut = nOut + 3
|
||||
nAcc = 0
|
||||
acc = 0
|
||||
end
|
||||
end
|
||||
|
||||
if nAcc == 3 then
|
||||
-- 3 hextets -> 16 bit output
|
||||
acc = acc * 64
|
||||
acc = floor(acc / 256)
|
||||
local b2 = acc % 256
|
||||
acc = floor(acc / 256)
|
||||
local b1 = acc % 256
|
||||
|
||||
out[nOut + 1] = char(b1)
|
||||
out[nOut + 2] = char(b2)
|
||||
elseif nAcc == 2 then
|
||||
-- 2 hextets -> 8 bit output
|
||||
acc = acc * 64
|
||||
acc = floor(acc / 256)
|
||||
acc = acc * 64
|
||||
acc = floor(acc / 256)
|
||||
local b1 = acc % 256
|
||||
|
||||
out[nOut + 1] = char(b1)
|
||||
elseif nAcc == 1 then
|
||||
error("Base64 has invalid length")
|
||||
end
|
||||
|
||||
return table.concat(out, "")
|
||||
end
|
||||
|
||||
return {
|
||||
decode = function(input: string)
|
||||
return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input)))
|
||||
end,
|
||||
encode = function(input: string)
|
||||
return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input)))
|
||||
end,
|
||||
}
|
||||
decode = decodeBase64,
|
||||
encode = encodeBase64,
|
||||
}
|
||||
29
plugin/rbx_dom_lua/base64.spec.lua
Normal 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
|
||||
@@ -1,99 +1,10 @@
|
||||
local CollectionService = game:GetService("CollectionService")
|
||||
local ScriptEditorService = game:GetService("ScriptEditorService")
|
||||
|
||||
local Error = require(script.Parent.Error)
|
||||
|
||||
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
||||
local TERRAIN_MATERIAL_COLORS = {
|
||||
Enum.Material.Grass,
|
||||
Enum.Material.Slate,
|
||||
Enum.Material.Concrete,
|
||||
Enum.Material.Brick,
|
||||
Enum.Material.Sand,
|
||||
Enum.Material.WoodPlanks,
|
||||
Enum.Material.Rock,
|
||||
Enum.Material.Glacier,
|
||||
Enum.Material.Snow,
|
||||
Enum.Material.Sandstone,
|
||||
Enum.Material.Mud,
|
||||
Enum.Material.Basalt,
|
||||
Enum.Material.Ground,
|
||||
Enum.Material.CrackedLava,
|
||||
Enum.Material.Asphalt,
|
||||
Enum.Material.Cobblestone,
|
||||
Enum.Material.Ice,
|
||||
Enum.Material.LeafyGrass,
|
||||
Enum.Material.Salt,
|
||||
Enum.Material.Limestone,
|
||||
Enum.Material.Pavement,
|
||||
}
|
||||
|
||||
local function isAttributeNameValid(attributeName)
|
||||
-- For SetAttribute to succeed, the attribute name must be less than or
|
||||
-- equal to 100 characters...
|
||||
return #attributeName <= 100
|
||||
-- ...and must only contain alphanumeric characters, periods, hyphens,
|
||||
-- underscores, or forward slashes.
|
||||
and attributeName:match("[^%w%.%-_/]") == nil
|
||||
end
|
||||
|
||||
local function isAttributeNameReserved(attributeName)
|
||||
-- For SetAttribute to succeed, attribute names must not use the RBX
|
||||
-- prefix, which is reserved by Roblox.
|
||||
return attributeName:sub(1, 3) == "RBX"
|
||||
end
|
||||
|
||||
-- Defines how to read and write properties that aren't directly scriptable.
|
||||
--
|
||||
-- The reflection database refers to these as having scriptability = "Custom"
|
||||
return {
|
||||
Instance = {
|
||||
Attributes = {
|
||||
read = function(instance)
|
||||
return true, instance:GetAttributes()
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
local existing = instance:GetAttributes()
|
||||
local didAllWritesSucceed = true
|
||||
|
||||
for attributeName, attributeValue in pairs(value) do
|
||||
if isAttributeNameReserved(attributeName) then
|
||||
-- If the attribute name is reserved, then we don't
|
||||
-- really care about reporting any failures about
|
||||
-- it.
|
||||
continue
|
||||
end
|
||||
|
||||
if not isAttributeNameValid(attributeName) then
|
||||
didAllWritesSucceed = false
|
||||
continue
|
||||
end
|
||||
|
||||
instance:SetAttribute(attributeName, attributeValue)
|
||||
end
|
||||
|
||||
for existingAttributeName in pairs(existing) do
|
||||
if isAttributeNameReserved(existingAttributeName) then
|
||||
continue
|
||||
end
|
||||
|
||||
if not isAttributeNameValid(existingAttributeName) then
|
||||
didAllWritesSucceed = false
|
||||
continue
|
||||
end
|
||||
|
||||
if value[existingAttributeName] == nil then
|
||||
instance:SetAttribute(existingAttributeName, nil)
|
||||
end
|
||||
end
|
||||
|
||||
return didAllWritesSucceed
|
||||
end,
|
||||
},
|
||||
Tags = {
|
||||
read = function(instance)
|
||||
return true, CollectionService:GetTags(instance)
|
||||
@@ -121,117 +32,13 @@ return {
|
||||
},
|
||||
LocalizationTable = {
|
||||
Contents = {
|
||||
read = function(instance, _)
|
||||
read = function(instance, key)
|
||||
return true, instance:GetContents()
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
write = function(instance, key, value)
|
||||
instance:SetContents(value)
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
Model = {
|
||||
Scale = {
|
||||
read = function(instance, _, _)
|
||||
return true, instance:GetScale()
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
return true, instance:ScaleTo(value)
|
||||
end,
|
||||
},
|
||||
WorldPivotData = {
|
||||
read = function(instance)
|
||||
return true, instance.WorldPivot
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
if value == nil then
|
||||
return true, nil
|
||||
else
|
||||
instance.WorldPivot = value
|
||||
return true
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
Terrain = {
|
||||
MaterialColors = {
|
||||
read = function(instance: Terrain)
|
||||
-- There's no way to get a list of every color, so we have to
|
||||
-- make one.
|
||||
local colors = {}
|
||||
for _, material in TERRAIN_MATERIAL_COLORS do
|
||||
colors[material] = instance:GetMaterialColor(material)
|
||||
end
|
||||
|
||||
return true, colors
|
||||
end,
|
||||
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
for material, color in value do
|
||||
instance:SetMaterialColor(material, color)
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
Script = {
|
||||
Source = {
|
||||
read = function(instance: Script)
|
||||
return true, ScriptEditorService:GetEditorSource(instance)
|
||||
end,
|
||||
write = function(instance: Script, _, value: string)
|
||||
task.spawn(function()
|
||||
ScriptEditorService:UpdateSourceAsync(instance, function()
|
||||
return value
|
||||
end)
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
ModuleScript = {
|
||||
Source = {
|
||||
read = function(instance: ModuleScript)
|
||||
return true, ScriptEditorService:GetEditorSource(instance)
|
||||
end,
|
||||
write = function(instance: ModuleScript, _, value: string)
|
||||
task.spawn(function()
|
||||
ScriptEditorService:UpdateSourceAsync(instance, function()
|
||||
return value
|
||||
end)
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
StyleRule = {
|
||||
PropertiesSerialize = {
|
||||
read = function(instance: StyleRule)
|
||||
return true, instance:GetProperties()
|
||||
end,
|
||||
write = function(instance: StyleRule, _, value: { [any]: any })
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
local existing = instance:GetProperties()
|
||||
|
||||
for itemName, itemValue in pairs(value) do
|
||||
instance:SetProperty(itemName, itemValue)
|
||||
end
|
||||
|
||||
for existingItemName in pairs(existing) do
|
||||
if value[existingItemName] == nil then
|
||||
instance:SetProperty(existingItemName, nil)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
|
||||
return PropertyDescriptor.fromRaw(
|
||||
currentClass.Properties[aliasData.AliasFor],
|
||||
currentClassName,
|
||||
aliasData.AliasFor
|
||||
)
|
||||
aliasData.AliasFor)
|
||||
end
|
||||
|
||||
return nil
|
||||
|
||||
7
plugin/rbx_dom_lua/init.spec.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
return function()
|
||||
local RbxDom = require(script.Parent)
|
||||
|
||||
it("should load", function()
|
||||
expect(RbxDom).to.be.ok()
|
||||
end)
|
||||
end
|
||||
@@ -1,11 +1,19 @@
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10))
|
||||
local TestEZ = require(ReplicatedStorage.TestEZ)
|
||||
|
||||
local Rojo = ReplicatedStorage.Rojo
|
||||
|
||||
local Settings = require(Rojo.Plugin.Settings)
|
||||
Settings:set("logLevel", "Trace")
|
||||
Settings:set("typecheckingEnabled", true)
|
||||
local DevSettings = require(Rojo.Plugin.DevSettings)
|
||||
|
||||
local setDevSettings = not DevSettings:hasChangedValues()
|
||||
|
||||
if setDevSettings then
|
||||
DevSettings:createTestSettings()
|
||||
end
|
||||
|
||||
require(Rojo.Plugin.runTests)(TestEZ)
|
||||
|
||||
if setDevSettings then
|
||||
DevSettings:resetValues()
|
||||
end
|
||||