Compare commits
1 Commits
master
...
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
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.lua]
|
[*.lua]
|
||||||
indent_style = tab
|
|
||||||
|
|
||||||
[*.luau]
|
|
||||||
indent_style = tab
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and Test
|
name: Build and Test
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm]
|
rust_version: [stable, 1.55.0]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rs/toolchain@v1
|
||||||
|
|
||||||
- name: Restore Rust Cache
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
with:
|
||||||
path: |
|
toolchain: ${{ matrix.rust_version }}
|
||||||
~/.cargo/registry
|
override: true
|
||||||
~/.cargo/git
|
profile: minimal
|
||||||
target
|
|
||||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --locked --verbose
|
run: cargo build --locked --verbose
|
||||||
@@ -41,93 +36,24 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --locked --verbose
|
run: cargo test --locked --verbose
|
||||||
|
|
||||||
- name: Save Rust Cache
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
msrv:
|
|
||||||
name: Check MSRV
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: dtolnay/rust-toolchain@1.88.0
|
|
||||||
|
|
||||||
- name: Restore Rust Cache
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --locked --verbose
|
|
||||||
|
|
||||||
- name: Save Rust Cache
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Rustfmt, Clippy, Stylua, & Selene
|
name: Rustfmt and Clippy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
- name: Restore Rust Cache
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Setup Rokit
|
|
||||||
uses: CompeyDev/setup-rokit@v0.1.2
|
|
||||||
with:
|
|
||||||
version: 'v1.1.0'
|
|
||||||
|
|
||||||
- name: Stylua
|
|
||||||
run: stylua --check plugin/src
|
|
||||||
|
|
||||||
- name: Selene
|
|
||||||
run: selene plugin/src
|
|
||||||
|
|
||||||
- name: Rustfmt
|
- name: Rustfmt
|
||||||
run: cargo fmt -- --check
|
run: cargo fmt -- --check
|
||||||
|
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy
|
run: cargo clippy
|
||||||
|
|
||||||
- name: Save Rust Cache
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
138
.github/workflows/release.yml
vendored
@@ -8,49 +8,52 @@ jobs:
|
|||||||
create-release:
|
create-release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
with:
|
||||||
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }}
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
build-plugin:
|
build-plugin:
|
||||||
needs: ["create-release"]
|
needs: ["create-release"]
|
||||||
name: Build Roblox Studio Plugin
|
name: Build Roblox Studio Plugin
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Setup Rokit
|
- name: Setup Foreman
|
||||||
uses: CompeyDev/setup-rokit@v0.1.2
|
uses: Roblox/setup-foreman@v1
|
||||||
with:
|
with:
|
||||||
version: 'v1.1.0'
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Plugin
|
- name: Build Plugin
|
||||||
run: rojo build plugin.project.json --output Rojo.rbxm
|
run: rojo build plugin --output Rojo.rbxm
|
||||||
|
|
||||||
- name: Upload Plugin to Release
|
- name: Upload Plugin to Release
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
with:
|
||||||
gh release upload ${{ github.ref_name }} Rojo.rbxm
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
|
asset_path: Rojo.rbxm
|
||||||
|
asset_name: Rojo.rbxm
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
- name: Upload Plugin to Artifacts
|
- name: Upload Plugin to Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Rojo.rbxm
|
name: Rojo.rbxm
|
||||||
path: Rojo.rbxm
|
path: Rojo.rbxm
|
||||||
|
|
||||||
- name: Upload Plugin to Roblox
|
|
||||||
env:
|
|
||||||
RBX_API_KEY: ${{ secrets.PLUGIN_UPLOAD_TOKEN }}
|
|
||||||
RBX_UNIVERSE_ID: ${{ vars.PLUGIN_CI_PLACE_ID }}
|
|
||||||
RBX_PLACE_ID: ${{ vars.PLUGIN_CI_UNIVERSE_ID }}
|
|
||||||
run: lune run upload-plugin Rojo.rbxm
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: ["create-release"]
|
needs: ["create-release"]
|
||||||
@@ -58,31 +61,25 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# https://doc.rust-lang.org/rustc/platform-support.html
|
# https://doc.rust-lang.org/rustc/platform-support.html
|
||||||
|
#
|
||||||
|
# FIXME: After the Rojo VS Code extension updates, add architecture
|
||||||
|
# names to each of these releases. We'll rename win64 to windows and add
|
||||||
|
# -x86_64 to each release.
|
||||||
include:
|
include:
|
||||||
- host: linux
|
- host: linux
|
||||||
os: ubuntu-22.04
|
os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
label: linux-x86_64
|
label: linux
|
||||||
|
|
||||||
- host: linux
|
|
||||||
os: ubuntu-22.04-arm
|
|
||||||
target: aarch64-unknown-linux-gnu
|
|
||||||
label: linux-aarch64
|
|
||||||
|
|
||||||
- host: windows
|
- host: windows
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
label: windows-x86_64
|
label: win64
|
||||||
|
|
||||||
- host: windows
|
|
||||||
os: windows-11-arm
|
|
||||||
target: aarch64-pc-windows-msvc
|
|
||||||
label: windows-aarch64
|
|
||||||
|
|
||||||
- host: macos
|
- host: macos
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
label: macos-x86_64
|
label: macos
|
||||||
|
|
||||||
- host: macos
|
- host: macos
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
@@ -94,64 +91,63 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BIN: rojo
|
BIN: rojo
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Get Version from Tag
|
||||||
uses: dtolnay/rust-toolchain@stable
|
shell: bash
|
||||||
with:
|
# https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
|
||||||
targets: ${{ matrix.target }}
|
run: |
|
||||||
|
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
echo "Version is: ${{ env.PROJECT_VERSION }}"
|
||||||
|
|
||||||
- name: Restore Rust Cache
|
- name: Install Rust
|
||||||
uses: actions/cache/restore@v4
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
path: |
|
toolchain: stable
|
||||||
~/.cargo/registry
|
target: ${{ matrix.target }}
|
||||||
~/.cargo/git
|
override: true
|
||||||
target
|
profile: minimal
|
||||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
run: cargo build --release --locked --verbose
|
||||||
|
|
||||||
- name: Save Rust Cache
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Generate Artifact Name
|
|
||||||
shell: bash
|
|
||||||
env:
|
env:
|
||||||
TAG_NAME: ${{ github.ref_name }}
|
# Build into a known directory so we can find our build artifact more
|
||||||
run: |
|
# easily.
|
||||||
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV"
|
CARGO_TARGET_DIR: output
|
||||||
|
|
||||||
- name: Create Archive and Upload to Release
|
# On platforms that use OpenSSL, ensure it is statically linked to
|
||||||
|
# make binaries more portable.
|
||||||
|
OPENSSL_STATIC: 1
|
||||||
|
|
||||||
|
- name: Create Release Archive
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
mkdir staging
|
mkdir staging
|
||||||
|
|
||||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||||
cp "target/${{ matrix.target }}/release/$BIN.exe" staging/
|
cp "output/release/$BIN.exe" staging/
|
||||||
cd staging
|
cd staging
|
||||||
7z a ../$ARTIFACT_NAME *
|
7z a ../release.zip *
|
||||||
else
|
else
|
||||||
cp "target/${{ matrix.target }}/release/$BIN" staging/
|
cp "output/release/$BIN" staging/
|
||||||
cd staging
|
cd staging
|
||||||
zip ../$ARTIFACT_NAME *
|
zip ../release.zip *
|
||||||
fi
|
fi
|
||||||
|
|
||||||
gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME
|
- name: Upload Archive to Release
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||||
|
asset_path: release.zip
|
||||||
|
asset_name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
- name: Upload Archive to Artifacts
|
- name: Upload Archive to Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.ARTIFACT_NAME }}
|
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
||||||
name: ${{ env.ARTIFACT_NAME }}
|
path: release.zip
|
||||||
12
.gitignore
vendored
@@ -10,8 +10,8 @@
|
|||||||
/*.rbxl
|
/*.rbxl
|
||||||
/*.rbxlx
|
/*.rbxlx
|
||||||
|
|
||||||
# Sourcemap for the Rojo plugin (for better intellisense)
|
# Test places for the Roblox Studio Plugin
|
||||||
/sourcemap.json
|
/plugin/*.rbxlx
|
||||||
|
|
||||||
# Roblox Studio holds 'lock' files on places
|
# Roblox Studio holds 'lock' files on places
|
||||||
*.rbxl.lock
|
*.rbxl.lock
|
||||||
@@ -20,9 +20,5 @@
|
|||||||
# Snapshot files from the 'insta' Rust crate
|
# Snapshot files from the 'insta' Rust crate
|
||||||
**/*.snap.new
|
**/*.snap.new
|
||||||
|
|
||||||
# Macos file system junk
|
# Selene generates a roblox.toml file that should not be checked in.
|
||||||
._*
|
/roblox.toml
|
||||||
.DS_STORE
|
|
||||||
|
|
||||||
# JetBrains IDEs
|
|
||||||
/.idea/
|
|
||||||
37
.gitmodules
vendored
@@ -1,24 +1,15 @@
|
|||||||
[submodule "plugin/Packages/Roact"]
|
[submodule "plugin/modules/roact"]
|
||||||
path = plugin/Packages/Roact
|
path = plugin/modules/roact
|
||||||
url = https://github.com/roblox/roact.git
|
url = https://github.com/Roblox/roact.git
|
||||||
[submodule "plugin/Packages/Flipper"]
|
[submodule "plugin/modules/testez"]
|
||||||
path = plugin/Packages/Flipper
|
path = plugin/modules/testez
|
||||||
url = https://github.com/reselim/flipper.git
|
url = https://github.com/Roblox/testez.git
|
||||||
[submodule "plugin/Packages/Promise"]
|
[submodule "plugin/modules/promise"]
|
||||||
path = plugin/Packages/Promise
|
path = plugin/modules/promise
|
||||||
url = https://github.com/evaera/roblox-lua-promise.git
|
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
||||||
[submodule "plugin/Packages/t"]
|
[submodule "plugin/modules/t"]
|
||||||
path = plugin/Packages/t
|
path = plugin/modules/t
|
||||||
url = https://github.com/osyrisrblx/t.git
|
url = https://github.com/osyrisrblx/t.git
|
||||||
[submodule "plugin/Packages/TestEZ"]
|
[submodule "plugin/modules/flipper"]
|
||||||
path = plugin/Packages/TestEZ
|
path = plugin/modules/flipper
|
||||||
url = https://github.com/roblox/testez.git
|
url = https://github.com/Reselim/Flipper
|
||||||
[submodule "plugin/Packages/Highlighter"]
|
|
||||||
path = plugin/Packages/Highlighter
|
|
||||||
url = https://github.com/boatbomber/highlighter.git
|
|
||||||
[submodule "plugin/Packages/msgpack-luau"]
|
|
||||||
path = plugin/Packages/msgpack-luau
|
|
||||||
url = https://github.com/cipharius/msgpack-luau/
|
|
||||||
[submodule ".lune/opencloud-execute"]
|
|
||||||
path = .lune/opencloud-execute
|
|
||||||
url = https://github.com/Dekkonot/opencloud-luau-execute-lune.git
|
|
||||||
|
|||||||
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",
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
return {
|
|
||||||
luau = {
|
|
||||||
languagemode = "strict",
|
|
||||||
aliases = {
|
|
||||||
lune = "~/.lune/.typedefs/0.10.4/",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
local args: any = ...
|
|
||||||
assert(args, "no arguments passed to script")
|
|
||||||
|
|
||||||
local input: buffer = args.BinaryInput
|
|
||||||
|
|
||||||
local AssetService = game:GetService("AssetService")
|
|
||||||
local SerializationService = game:GetService("SerializationService")
|
|
||||||
local EncodingService = game:GetService("EncodingService")
|
|
||||||
|
|
||||||
local input_hash: buffer = EncodingService:ComputeBufferHash(input, Enum.HashAlgorithm.Sha256)
|
|
||||||
local hex_hash: { string } = table.create(buffer.len(input_hash))
|
|
||||||
for i = 0, buffer.len(input_hash) - 1 do
|
|
||||||
table.insert(hex_hash, string.format("%02x", buffer.readu8(input_hash, i)))
|
|
||||||
end
|
|
||||||
|
|
||||||
print(`Deserializing plugin file (size: {buffer.len(input)} bytes, hash: {table.concat(hex_hash, "")})`)
|
|
||||||
local plugin = SerializationService:DeserializeInstancesAsync(input)[1]
|
|
||||||
|
|
||||||
local UploadDetails = require(plugin.UploadDetails) :: any
|
|
||||||
local PLUGIN_ID = UploadDetails.assetId
|
|
||||||
local PLUGIN_NAME = UploadDetails.name
|
|
||||||
local PLUGIN_DESCRIPTION = UploadDetails.description
|
|
||||||
local PLUGIN_CREATOR_ID = UploadDetails.creatorId
|
|
||||||
local PLUGIN_CREATOR_TYPE = UploadDetails.creatorType
|
|
||||||
|
|
||||||
assert(typeof(PLUGIN_ID) == "number", "UploadDetails did not contain a number field 'assetId'")
|
|
||||||
assert(typeof(PLUGIN_NAME) == "string", "UploadDetails did not contain a string field 'name'")
|
|
||||||
assert(typeof(PLUGIN_DESCRIPTION) == "string", "UploadDetails did not contain a string field 'description'")
|
|
||||||
assert(typeof(PLUGIN_CREATOR_ID) == "number", "UploadDetails did not contain a number field 'creatorId'")
|
|
||||||
assert(typeof(PLUGIN_CREATOR_TYPE) == "string", "UploadDetails did not contain a string field 'creatorType'")
|
|
||||||
assert(
|
|
||||||
Enum.AssetCreatorType:FromName(PLUGIN_CREATOR_TYPE) ~= nil,
|
|
||||||
"UploadDetails field 'creatorType' was not a valid member of Enum.AssetCreatorType"
|
|
||||||
)
|
|
||||||
|
|
||||||
print(`Uploading to {PLUGIN_ID}`)
|
|
||||||
print(`Plugin Name: {PLUGIN_NAME}`)
|
|
||||||
print(`Plugin Description: {PLUGIN_DESCRIPTION}`)
|
|
||||||
|
|
||||||
local result, version_or_err = AssetService:CreateAssetVersionAsync(plugin, Enum.AssetType.Plugin, PLUGIN_ID, {
|
|
||||||
["Name"] = PLUGIN_NAME,
|
|
||||||
["Description"] = PLUGIN_DESCRIPTION,
|
|
||||||
["CreatorId"] = PLUGIN_CREATOR_ID,
|
|
||||||
["CreatorType"] = Enum.AssetCreatorType:FromName(PLUGIN_CREATOR_TYPE),
|
|
||||||
})
|
|
||||||
|
|
||||||
if result ~= Enum.CreateAssetResult.Success then
|
|
||||||
error(`Plugin failed to upload because: {result.Name} - {version_or_err}`)
|
|
||||||
end
|
|
||||||
|
|
||||||
print(`Plugin uploaded successfully. New version is {version_or_err}.`)
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
local fs = require("@lune/fs")
|
|
||||||
local process = require("@lune/process")
|
|
||||||
local stdio = require("@lune/stdio")
|
|
||||||
|
|
||||||
local luau_execute = require("./opencloud-execute")
|
|
||||||
|
|
||||||
local UNIVERSE_ID = process.env["RBX_UNIVERSE_ID"]
|
|
||||||
local PLACE_ID = process.env["RBX_PLACE_ID"]
|
|
||||||
|
|
||||||
local version_string = fs.readFile("plugin/Version.txt")
|
|
||||||
local versions = { string.match(version_string, "^v?(%d+)%.(%d+)%.(%d+)(.*)$") }
|
|
||||||
if versions[4] ~= "" then
|
|
||||||
print("This release is a pre-release. Skipping uploading plugin.")
|
|
||||||
process.exit(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
local plugin_path = process.args[1]
|
|
||||||
assert(
|
|
||||||
typeof(plugin_path) == "string",
|
|
||||||
"no plugin path provided, expected usage is `lune run upload-plugin [PATH TO RBXM]`."
|
|
||||||
)
|
|
||||||
|
|
||||||
-- For local testing
|
|
||||||
if process.env["CI"] ~= "true" then
|
|
||||||
local rojo = process.exec("rojo", { "build", "plugin.project.json", "--output", plugin_path })
|
|
||||||
if not rojo.ok then
|
|
||||||
stdio.ewrite("plugin upload failed because: could not build plugin.rbxm\n\n")
|
|
||||||
stdio.ewrite(rojo.stderr)
|
|
||||||
stdio.ewrite("\n")
|
|
||||||
process.exit(1)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
assert(fs.isFile(plugin_path), `Plugin file did not exist at {plugin_path}`)
|
|
||||||
end
|
|
||||||
local plugin_content = fs.readFile(plugin_path)
|
|
||||||
|
|
||||||
local engine_script = fs.readFile(".lune/scripts/plugin-upload.luau")
|
|
||||||
|
|
||||||
print("Creating task to upload plugin")
|
|
||||||
local task = luau_execute.create_task_latest(UNIVERSE_ID, PLACE_ID, engine_script, 300, false, plugin_content)
|
|
||||||
|
|
||||||
print("Waiting for task to finish")
|
|
||||||
local success = luau_execute.await_finish(task)
|
|
||||||
if not success then
|
|
||||||
local error = luau_execute.get_error(task)
|
|
||||||
assert(error, "could not fetch error from task")
|
|
||||||
stdio.ewrite("plugin upload failed because: task did not finish successfully\n\n")
|
|
||||||
stdio.ewrite(error.code)
|
|
||||||
stdio.ewrite("\n")
|
|
||||||
stdio.ewrite(error.message)
|
|
||||||
stdio.ewrite("\n")
|
|
||||||
process.exit(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Output from task:\n")
|
|
||||||
for _, log in luau_execute.get_structured_logs(task) do
|
|
||||||
if log.messageType == "ERROR" then
|
|
||||||
stdio.write(stdio.color("red"))
|
|
||||||
stdio.write(log.message)
|
|
||||||
stdio.write("\n")
|
|
||||||
stdio.write(stdio.color("reset"))
|
|
||||||
elseif log.messageType == "INFO" then
|
|
||||||
stdio.write(stdio.color("cyan"))
|
|
||||||
stdio.write(log.message)
|
|
||||||
stdio.write("\n")
|
|
||||||
stdio.write(stdio.color("reset"))
|
|
||||||
elseif log.messageType == "WARNING" then
|
|
||||||
stdio.write(stdio.color("yellow"))
|
|
||||||
stdio.write(log.message)
|
|
||||||
stdio.write("\n")
|
|
||||||
stdio.write(stdio.color("reset"))
|
|
||||||
else
|
|
||||||
stdio.write(stdio.color("reset"))
|
|
||||||
stdio.write(log.message)
|
|
||||||
stdio.write("\n")
|
|
||||||
stdio.write(stdio.color("reset"))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
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
|
|
||||||
}
|
|
||||||
1182
CHANGELOG.md
@@ -14,31 +14,13 @@ Code contributions are welcome for features and bugs that have been reported in
|
|||||||
You'll want these tools to work on Rojo:
|
You'll want these tools to work on Rojo:
|
||||||
|
|
||||||
* Latest stable Rust compiler
|
* Latest stable Rust compiler
|
||||||
* Rustfmt and Clippy are used for code formatting and linting.
|
|
||||||
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
||||||
* [Rokit](https://github.com/rojo-rbx/rokit)
|
* [Foreman](https://github.com/Roblox/foreman)
|
||||||
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
|
|
||||||
|
|
||||||
When working on the Studio plugin, we recommend using this command to automatically rebuild the plugin when you save a change:
|
|
||||||
|
|
||||||
*(Make sure you've enabled the Studio setting to reload plugins on file change!)*
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/watch-build-plugin.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also run the plugin's unit tests with the following:
|
|
||||||
|
|
||||||
*(Make sure you have `run-in-roblox` installed first!)*
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/unit-test-plugin.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
Documentation impacts way more people than the individual lines of code we write.
|
Documentation impacts way more people than the individual lines of code we write.
|
||||||
|
|
||||||
If you find any problems in the documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
If you find any problems in documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
||||||
|
|
||||||
## Bug Reports and Feature Requests
|
## Bug Reports and Feature Requests
|
||||||
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.
|
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.
|
||||||
|
|||||||
2638
Cargo.lock
generated
119
Cargo.toml
@@ -1,22 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "7.7.0-rc.1"
|
version = "7.1.1"
|
||||||
rust-version = "1.88"
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
authors = [
|
|
||||||
"Lucien Greathouse <me@lpghatguy.com>",
|
|
||||||
"Micah Reid <git@dekkonot.com>",
|
|
||||||
"Ken Loeffler <kenloef@gmail.com>",
|
|
||||||
]
|
|
||||||
description = "Enables professional-grade development tools for Roblox developers"
|
description = "Enables professional-grade development tools for Roblox developers"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
homepage = "https://rojo.space"
|
homepage = "https://rojo.space"
|
||||||
documentation = "https://rojo.space/docs"
|
documentation = "https://rojo.space/docs"
|
||||||
repository = "https://github.com/rojo-rbx/rojo"
|
repository = "https://github.com/rojo-rbx/rojo"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
edition = "2021"
|
edition = "2018"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
exclude = ["/test-projects/**"]
|
exclude = [
|
||||||
|
"/test-projects/**",
|
||||||
|
]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
@@ -30,10 +27,6 @@ default = []
|
|||||||
# Enable this feature to live-reload assets from the web UI.
|
# Enable this feature to live-reload assets from the web UI.
|
||||||
dev_live_assets = []
|
dev_live_assets = []
|
||||||
|
|
||||||
# Run Rojo with this feature to open a Tracy session.
|
|
||||||
# Currently uses protocol v63, last supported in Tracy 0.9.1.
|
|
||||||
profile-with-tracy = ["profiling/profile-with-tracy"]
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
@@ -46,88 +39,68 @@ name = "build"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[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
|
# These dependencies can be uncommented when working on rbx-dom simultaneously
|
||||||
# rbx_binary = { path = "../rbx-dom/rbx_binary", features = [
|
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
|
||||||
# "unstable_text_format",
|
|
||||||
# ] }
|
|
||||||
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
|
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
|
||||||
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
|
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
|
||||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||||
|
|
||||||
rbx_binary = { version = "2.0.1", features = ["unstable_text_format"] }
|
rbx_binary = "0.6.4"
|
||||||
rbx_dom_weak = "4.1.0"
|
rbx_dom_weak = "2.3.0"
|
||||||
rbx_reflection = "6.1.0"
|
rbx_reflection = "4.2.0"
|
||||||
rbx_reflection_database = "2.0.2"
|
rbx_reflection_database = "0.2.2"
|
||||||
rbx_xml = "2.0.1"
|
rbx_xml = "0.12.3"
|
||||||
|
|
||||||
anyhow = "1.0.80"
|
anyhow = "1.0.44"
|
||||||
backtrace = "0.3.69"
|
backtrace = "0.3.61"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
crossbeam-channel = "0.5.12"
|
crossbeam-channel = "0.5.1"
|
||||||
csv = "1.3.0"
|
csv = "1.1.6"
|
||||||
env_logger = "0.9.3"
|
env_logger = "0.9.0"
|
||||||
fs-err = "2.11.0"
|
fs-err = "2.6.0"
|
||||||
futures = "0.3.30"
|
futures = "0.3.17"
|
||||||
globset = "0.4.14"
|
globset = "0.4.8"
|
||||||
humantime = "2.1.0"
|
humantime = "2.1.0"
|
||||||
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
|
||||||
hyper-tungstenite = "0.11.0"
|
|
||||||
jod-thread = "0.1.2"
|
jod-thread = "0.1.2"
|
||||||
log = "0.4.21"
|
log = "0.4.14"
|
||||||
num_cpus = "1.16.0"
|
maplit = "1.0.2"
|
||||||
opener = "0.5.2"
|
notify = "4.0.17"
|
||||||
rayon = "1.9.0"
|
opener = "0.5.0"
|
||||||
reqwest = { version = "0.11.24", default-features = false, features = [
|
reqwest = { version = "0.11.10", features = ["blocking", "json"] }
|
||||||
"blocking",
|
|
||||||
"json",
|
|
||||||
"rustls-tls",
|
|
||||||
] }
|
|
||||||
ritz = "0.1.0"
|
ritz = "0.1.0"
|
||||||
roblox_install = "1.0.0"
|
roblox_install = "1.0.0"
|
||||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.68"
|
||||||
jsonc-parser = { version = "0.27.0", features = ["serde"] }
|
termcolor = "1.1.2"
|
||||||
strum = { version = "0.27", features = ["derive"] }
|
thiserror = "1.0.30"
|
||||||
toml = "0.5.11"
|
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
|
||||||
termcolor = "1.4.1"
|
uuid = { version = "1.0.0", features = ["v4", "serde"] }
|
||||||
thiserror = "1.0.57"
|
clap = { version = "3.1.18", features = ["derive"] }
|
||||||
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"
|
|
||||||
pathdiff = "0.2.3"
|
|
||||||
|
|
||||||
blake3 = "1.5.0"
|
|
||||||
float-cmp = "0.9.0"
|
|
||||||
indexmap = { version = "2.10.0", features = ["serde"] }
|
|
||||||
rmp-serde = "1.3.0"
|
|
||||||
serde_bytes = "0.11.19"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.10.1"
|
winreg = "0.10.1"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
memofs = { version = "0.3.0", path = "crates/memofs" }
|
memofs = { version = "0.2.0", path = "crates/memofs" }
|
||||||
|
|
||||||
embed-resource = "1.8.0"
|
embed-resource = "1.6.4"
|
||||||
anyhow = "1.0.80"
|
anyhow = "1.0.44"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
fs-err = "2.11.0"
|
fs-err = "2.6.0"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
semver = "1.0.22"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
||||||
|
|
||||||
criterion = "0.3.6"
|
criterion = "0.3.5"
|
||||||
insta = { version = "1.36.1", features = ["redactions", "yaml", "json"] }
|
insta = { version = "1.8.0", features = ["redactions"] }
|
||||||
paste = "1.0.14"
|
paste = "1.0.5"
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.2.1"
|
||||||
serde_yaml = "0.8.26"
|
serde_yaml = "0.8.21"
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.2.0"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.3.2"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://rojo.space"><img src="assets/brand_images/logo-512.png" alt="Rojo" height="217" /></a>
|
<a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div> </div>
|
<div> </div>
|
||||||
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
|||||||
|
|
||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
Rojo supports Rust 1.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
Rojo supports Rust 1.46.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
To build this library, use:
|
To build this library or plugin, use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rojo build -o "{project_name}.rbxmx"
|
rojo build -o "{project_name}.rbxmx"
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
# Roblox Studio lock files
|
# Roblox Studio lock files
|
||||||
/*.rbxlx.lock
|
/*.rbxlx.lock
|
||||||
/*.rbxl.lock
|
/*.rbxl.lock
|
||||||
|
|
||||||
sourcemap.json
|
|
||||||
@@ -2,4 +2,4 @@ return {
|
|||||||
hello = function()
|
hello = function()
|
||||||
print("Hello world, from {project_name}!")
|
print("Hello world, from {project_name}!")
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"$className": "DataModel",
|
"$className": "DataModel",
|
||||||
|
|
||||||
"ReplicatedStorage": {
|
"ReplicatedStorage": {
|
||||||
"Shared": {
|
"Common": {
|
||||||
"$path": "src/shared"
|
"$path": "src/shared"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3,6 +3,4 @@
|
|||||||
|
|
||||||
# Roblox Studio lock files
|
# Roblox Studio lock files
|
||||||
/*.rbxlx.lock
|
/*.rbxlx.lock
|
||||||
/*.rbxl.lock
|
/*.rbxl.lock
|
||||||
|
|
||||||
sourcemap.json
|
|
||||||
|
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;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #e7e7e7
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width:100%;
|
max-width:100%;
|
||||||
max-height: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 criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||||
use tempfile::{tempdir, TempDir};
|
use tempfile::{tempdir, TempDir};
|
||||||
|
|
||||||
use librojo::cli::BuildCommand;
|
use librojo::cli::{build, BuildCommand};
|
||||||
|
|
||||||
pub fn benchmark_small_place(c: &mut Criterion) {
|
pub fn benchmark_small_place(c: &mut Criterion) {
|
||||||
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
|
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
|
||||||
@@ -20,7 +20,7 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
|
|||||||
group.bench_function("build", |b| {
|
group.bench_function("build", |b| {
|
||||||
b.iter_batched(
|
b.iter_batched(
|
||||||
|| place_setup(path),
|
|| place_setup(path),
|
||||||
|(_dir, options)| options.run().unwrap(),
|
|(_dir, options)| build(options).unwrap(),
|
||||||
BatchSize::SmallInput,
|
BatchSize::SmallInput,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -31,12 +31,11 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
|
|||||||
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
|
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let input = input_path.as_ref().to_path_buf();
|
let input = input_path.as_ref().to_path_buf();
|
||||||
let output = Some(dir.path().join("output.rbxlx"));
|
let output = dir.path().join("output.rbxlx");
|
||||||
|
|
||||||
let options = BuildCommand {
|
let options = BuildCommand {
|
||||||
project: input,
|
project: input,
|
||||||
watch: false,
|
watch: false,
|
||||||
plugin: None,
|
|
||||||
output,
|
output,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
66
build.rs
@@ -7,7 +7,6 @@ use fs_err as fs;
|
|||||||
use fs_err::File;
|
use fs_err::File;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use memofs::VfsSnapshot;
|
use memofs::VfsSnapshot;
|
||||||
use semver::Version;
|
|
||||||
|
|
||||||
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||||
println!("cargo:rerun-if-changed={}", path.display());
|
println!("cargo:rerun-if-changed={}", path.display());
|
||||||
@@ -20,18 +19,9 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
|||||||
|
|
||||||
let file_name = entry.file_name().to_str().unwrap().to_owned();
|
let file_name = entry.file_name().to_str().unwrap().to_owned();
|
||||||
|
|
||||||
if file_name.starts_with(".git") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can skip any TestEZ test files since they aren't necessary for
|
// We can skip any TestEZ test files since they aren't necessary for
|
||||||
// the plugin to run.
|
// the plugin to run.
|
||||||
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
|
if file_name.ends_with(".spec.lua") {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore images in msgpack-luau because they aren't UTF-8 encoded.
|
|
||||||
if file_name.ends_with(".png") {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,40 +40,38 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
|||||||
fn main() -> Result<(), anyhow::Error> {
|
fn main() -> Result<(), anyhow::Error> {
|
||||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||||
|
|
||||||
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||||
let plugin_dir = root_dir.join("plugin");
|
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
||||||
let templates_dir = root_dir.join("assets").join("project-templates");
|
|
||||||
|
|
||||||
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
let plugin_modules = plugin_root.join("modules");
|
||||||
let plugin_version =
|
|
||||||
Version::parse(fs::read_to_string(plugin_dir.join("Version.txt"))?.trim())?;
|
|
||||||
|
|
||||||
assert_eq!(
|
let snapshot = VfsSnapshot::dir(hashmap! {
|
||||||
our_version, plugin_version,
|
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
||||||
"plugin version does not match Cargo version"
|
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
||||||
);
|
"http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
|
||||||
|
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
||||||
let template_snapshot = snapshot_from_fs_path(&templates_dir)?;
|
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
||||||
|
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
||||||
let plugin_snapshot = VfsSnapshot::dir(hashmap! {
|
"modules" => VfsSnapshot::dir(hashmap! {
|
||||||
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?,
|
"roact" => VfsSnapshot::dir(hashmap! {
|
||||||
"plugin" => VfsSnapshot::dir(hashmap! {
|
"src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))?
|
||||||
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
|
}),
|
||||||
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
|
"promise" => VfsSnapshot::dir(hashmap! {
|
||||||
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
|
"lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))?
|
||||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
|
}),
|
||||||
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
"t" => VfsSnapshot::dir(hashmap! {
|
||||||
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
|
||||||
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
|
}),
|
||||||
"UploadDetails.json" => snapshot_from_fs_path(&plugin_dir.join("UploadDetails.json"))?,
|
"flipper" => VfsSnapshot::dir(hashmap! {
|
||||||
|
"src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))?
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
let template_file = File::create(Path::new(&out_dir).join("templates.bincode"))?;
|
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||||
let plugin_file = File::create(Path::new(&out_dir).join("plugin.bincode"))?;
|
let out_file = File::create(&out_path)?;
|
||||||
|
|
||||||
bincode::serialize_into(plugin_file, &plugin_snapshot)?;
|
bincode::serialize_into(out_file, &snapshot)?;
|
||||||
bincode::serialize_into(template_file, &template_snapshot)?;
|
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
|
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
|
||||||
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");
|
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");
|
||||||
|
|||||||
@@ -1,21 +1,6 @@
|
|||||||
# memofs Changelog
|
# memofs Changelog
|
||||||
|
|
||||||
## Unreleased Changes
|
## 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)
|
## 0.2.0 (2021-08-23)
|
||||||
* Updated to `crossbeam-channel` 0.5.1.
|
* 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.
|
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
|
||||||
|
|
||||||
## 0.1.0 (2020-03-10)
|
## 0.1.0 (2020-03-10)
|
||||||
* Initial release
|
* Initial release
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "memofs"
|
name = "memofs"
|
||||||
description = "Virtual filesystem with configurable backends."
|
description = "Virtual filesystem with configurable backends."
|
||||||
version = "0.3.1"
|
version = "0.2.0"
|
||||||
authors = [
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
"Lucien Greathouse <me@lpghatguy.com>",
|
|
||||||
"Micah Reid <git@dekkonot.com>",
|
|
||||||
"Ken Loeffler <kenloef@gmail.com>",
|
|
||||||
]
|
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -15,10 +11,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossbeam-channel = "0.5.12"
|
crossbeam-channel = "0.5.1"
|
||||||
fs-err = "2.11.0"
|
fs-err = "2.3.0"
|
||||||
notify = "4.0.17"
|
notify = "4.0.15"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3.10.1"
|
|
||||||
|
|||||||
@@ -50,12 +50,6 @@ impl InMemoryFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for InMemoryFs {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct InMemoryFsInner {
|
struct InMemoryFsInner {
|
||||||
entries: HashMap<PathBuf, Entry>,
|
entries: HashMap<PathBuf, Entry>,
|
||||||
@@ -157,11 +151,6 @@ impl VfsBackend for InMemoryFs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exists(&mut self, path: &Path) -> io::Result<bool> {
|
|
||||||
let inner = self.inner.lock().unwrap();
|
|
||||||
Ok(inner.entries.contains_key(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||||
let inner = self.inner.lock().unwrap();
|
let inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
@@ -181,21 +170,6 @@ impl VfsBackend for InMemoryFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
|
|
||||||
let mut inner = self.inner.lock().unwrap();
|
|
||||||
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
|
||||||
let mut inner = self.inner.lock().unwrap();
|
|
||||||
let mut path_buf = path.to_path_buf();
|
|
||||||
while let Some(parent) = path_buf.parent() {
|
|
||||||
inner.load_snapshot(parent.to_path_buf(), VfsSnapshot::empty_dir())?;
|
|
||||||
path_buf.pop();
|
|
||||||
}
|
|
||||||
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||||
let mut inner = self.inner.lock().unwrap();
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
@@ -232,33 +206,6 @@ impl VfsBackend for InMemoryFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We rely on Rojo to prepend cwd to any relative path before storing paths
|
|
||||||
// in MemoFS. The current implementation will error if no prepended absolute path
|
|
||||||
// is found. It really only normalizes paths within the provided path's context.
|
|
||||||
// Example: "/Users/username/project/../other/file.txt" ->
|
|
||||||
// "/Users/username/other/file.txt"
|
|
||||||
// Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt"
|
|
||||||
// This is not very robust. We should implement proper path normalization here or otherwise
|
|
||||||
// warn if we are missing context and can not fully canonicalize the path correctly.
|
|
||||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
|
||||||
let mut normalized = PathBuf::new();
|
|
||||||
for component in path.components() {
|
|
||||||
match component {
|
|
||||||
std::path::Component::ParentDir => {
|
|
||||||
normalized.pop();
|
|
||||||
}
|
|
||||||
std::path::Component::CurDir => {}
|
|
||||||
_ => normalized.push(component),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inner = self.inner.lock().unwrap();
|
|
||||||
match inner.entries.get(&normalized) {
|
|
||||||
Some(_) => Ok(normalized),
|
|
||||||
None => not_found(&normalized),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
let inner = self.inner.lock().unwrap();
|
let inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
@@ -275,17 +222,23 @@ impl VfsBackend for InMemoryFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn must_be_file<T>(path: &Path) -> io::Result<T> {
|
fn must_be_file<T>(path: &Path) -> io::Result<T> {
|
||||||
Err(io::Error::other(format!(
|
Err(io::Error::new(
|
||||||
"path {} was a directory, but must be a file",
|
io::ErrorKind::Other,
|
||||||
path.display()
|
format!(
|
||||||
)))
|
"path {} was a directory, but must be a file",
|
||||||
|
path.display()
|
||||||
|
),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn must_be_dir<T>(path: &Path) -> io::Result<T> {
|
fn must_be_dir<T>(path: &Path) -> io::Result<T> {
|
||||||
Err(io::Error::other(format!(
|
Err(io::Error::new(
|
||||||
"path {} was a file, but must be a directory",
|
io::ErrorKind::Other,
|
||||||
path.display()
|
format!(
|
||||||
)))
|
"path {} was a file, but must be a directory",
|
||||||
|
path.display()
|
||||||
|
),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn not_found<T>(path: &Path) -> io::Result<T> {
|
fn not_found<T>(path: &Path) -> io::Result<T> {
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ mod noop_backend;
|
|||||||
mod snapshot;
|
mod snapshot;
|
||||||
mod std_backend;
|
mod std_backend;
|
||||||
|
|
||||||
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
use std::{io, str};
|
|
||||||
|
|
||||||
pub use in_memory_fs::InMemoryFs;
|
pub use in_memory_fs::InMemoryFs;
|
||||||
pub use noop_backend::NoopBackend;
|
pub use noop_backend::NoopBackend;
|
||||||
@@ -70,14 +70,10 @@ impl<T> IoResultExt<T> for io::Result<T> {
|
|||||||
pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
||||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
|
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
|
||||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
|
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
|
||||||
fn exists(&mut self, path: &Path) -> io::Result<bool>;
|
|
||||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
|
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
|
||||||
fn create_dir(&mut self, path: &Path) -> io::Result<()>;
|
|
||||||
fn create_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
|
||||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
||||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf>;
|
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
||||||
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
||||||
@@ -159,29 +155,6 @@ impl VfsInner {
|
|||||||
Ok(Arc::new(contents))
|
Ok(Arc::new(contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_to_string<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<String>> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
let contents = self.backend.read(path)?;
|
|
||||||
|
|
||||||
if self.watch_enabled {
|
|
||||||
self.backend.watch(path)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contents_str = str::from_utf8(&contents).map_err(|_| {
|
|
||||||
io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!("File was not valid UTF-8: {}", path.display()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Arc::new(contents_str.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exists<P: AsRef<Path>>(&mut self, path: P) -> io::Result<bool> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.backend.exists(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let contents = contents.as_ref();
|
let contents = contents.as_ref();
|
||||||
@@ -199,16 +172,6 @@ impl VfsInner {
|
|||||||
Ok(dir)
|
Ok(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.backend.create_dir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.backend.create_dir_all(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let _ = self.backend.unwatch(path);
|
let _ = self.backend.unwatch(path);
|
||||||
@@ -226,18 +189,16 @@ impl VfsInner {
|
|||||||
self.backend.metadata(path)
|
self.backend.metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonicalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.backend.canonicalize(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
self.backend.event_receiver()
|
self.backend.event_receiver()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
||||||
if let VfsEvent::Remove(path) = event {
|
match event {
|
||||||
let _ = self.backend.unwatch(path);
|
VfsEvent::Remove(path) => {
|
||||||
|
let _ = self.backend.unwatch(&path);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -300,33 +261,6 @@ impl Vfs {
|
|||||||
self.inner.lock().unwrap().read(path)
|
self.inner.lock().unwrap().read(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a file from the VFS (or from the underlying backend if it isn't
|
|
||||||
/// resident) into a string.
|
|
||||||
///
|
|
||||||
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string].
|
|
||||||
///
|
|
||||||
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
|
|
||||||
#[inline]
|
|
||||||
pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.lock().unwrap().read_to_string(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a file from the VFS (or the underlying backend if it isn't
|
|
||||||
/// resident) into a string, and normalize its line endings to LF.
|
|
||||||
///
|
|
||||||
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string], but also performs
|
|
||||||
/// line ending normalization.
|
|
||||||
///
|
|
||||||
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
|
|
||||||
#[inline]
|
|
||||||
pub fn read_to_string_lf_normalized<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
let contents = self.inner.lock().unwrap().read_to_string(path)?;
|
|
||||||
|
|
||||||
Ok(contents.replace("\r\n", "\n").into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a file to the VFS and the underlying backend.
|
/// Write a file to the VFS and the underlying backend.
|
||||||
///
|
///
|
||||||
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
||||||
@@ -350,42 +284,6 @@ impl Vfs {
|
|||||||
self.inner.lock().unwrap().read_dir(path)
|
self.inner.lock().unwrap().read_dir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether the given path exists.
|
|
||||||
///
|
|
||||||
/// Roughly equivalent to [`std::fs::exists`][std::fs::exists].
|
|
||||||
///
|
|
||||||
/// [std::fs::exists]: https://doc.rust-lang.org/stable/std/fs/fn.exists.html
|
|
||||||
#[inline]
|
|
||||||
pub fn exists<P: AsRef<Path>>(&self, path: P) -> io::Result<bool> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.lock().unwrap().exists(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a directory at the provided location.
|
|
||||||
///
|
|
||||||
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
|
|
||||||
/// Similiar to that function, this function will fail if the parent of the
|
|
||||||
/// path does not exist.
|
|
||||||
///
|
|
||||||
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
|
|
||||||
#[inline]
|
|
||||||
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.lock().unwrap().create_dir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a directory at the provided location, recursively creating
|
|
||||||
/// all parent components if they are missing.
|
|
||||||
///
|
|
||||||
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
|
|
||||||
///
|
|
||||||
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
|
|
||||||
#[inline]
|
|
||||||
pub fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.lock().unwrap().create_dir_all(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a file.
|
/// Remove a file.
|
||||||
///
|
///
|
||||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||||
@@ -419,19 +317,6 @@ impl Vfs {
|
|||||||
self.inner.lock().unwrap().metadata(path)
|
self.inner.lock().unwrap().metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normalize a path via the underlying backend.
|
|
||||||
///
|
|
||||||
/// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are
|
|
||||||
/// resolved against the backend's current working directory (if applicable) and errors are
|
|
||||||
/// surfaced directly from the backend.
|
|
||||||
///
|
|
||||||
/// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
|
|
||||||
#[inline]
|
|
||||||
pub fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.lock().unwrap().canonicalize(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
@@ -501,31 +386,6 @@ impl VfsLock<'_> {
|
|||||||
self.inner.read_dir(path)
|
self.inner.read_dir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a directory at the provided location.
|
|
||||||
///
|
|
||||||
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
|
|
||||||
/// Similiar to that function, this function will fail if the parent of the
|
|
||||||
/// path does not exist.
|
|
||||||
///
|
|
||||||
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
|
|
||||||
#[inline]
|
|
||||||
pub fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.create_dir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a directory at the provided location, recursively creating
|
|
||||||
/// all parent components if they are missing.
|
|
||||||
///
|
|
||||||
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
|
|
||||||
///
|
|
||||||
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
|
|
||||||
#[inline]
|
|
||||||
pub fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.create_dir_all(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a file.
|
/// Remove a file.
|
||||||
///
|
///
|
||||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||||
@@ -559,13 +419,6 @@ impl VfsLock<'_> {
|
|||||||
self.inner.metadata(path)
|
self.inner.metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normalize a path via the underlying backend.
|
|
||||||
#[inline]
|
|
||||||
pub fn normalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.canonicalize(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
@@ -578,83 +431,3 @@ impl VfsLock<'_> {
|
|||||||
self.inner.commit_event(event)
|
self.inner.commit_event(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use crate::{InMemoryFs, StdBackend, Vfs, VfsSnapshot};
|
|
||||||
use std::io;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// https://github.com/rojo-rbx/rojo/issues/899
|
|
||||||
#[test]
|
|
||||||
fn read_to_string_lf_normalized_keeps_trailing_newline() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("test", VfsSnapshot::file("bar\r\nfoo\r\n\r\n"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
|
|
||||||
"bar\nfoo\n\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// https://github.com/rojo-rbx/rojo/issues/1200
|
|
||||||
#[test]
|
|
||||||
fn canonicalize_in_memory_success() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
|
||||||
|
|
||||||
imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string()))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
vfs.canonicalize("/test/nested/../file.txt").unwrap(),
|
|
||||||
PathBuf::from("/test/file.txt")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap())
|
|
||||||
.unwrap()
|
|
||||||
.to_string(),
|
|
||||||
contents.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn canonicalize_in_memory_missing_errors() {
|
|
||||||
let imfs = InMemoryFs::new();
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
let err = vfs.canonicalize("test").unwrap_err();
|
|
||||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn canonicalize_std_backend_success() {
|
|
||||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
|
||||||
let file_path = dir.path().join("file.txt");
|
|
||||||
fs_err::write(&file_path, contents.to_string()).unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(StdBackend::new());
|
|
||||||
let canonicalized = vfs.canonicalize(&file_path).unwrap();
|
|
||||||
assert_eq!(canonicalized, file_path.canonicalize().unwrap());
|
|
||||||
assert_eq!(
|
|
||||||
vfs.read_to_string(&canonicalized).unwrap().to_string(),
|
|
||||||
contents.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn canonicalize_std_backend_missing_errors() {
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
|
||||||
let file_path = dir.path().join("test");
|
|
||||||
|
|
||||||
let vfs = Vfs::new(StdBackend::new());
|
|
||||||
let err = vfs.canonicalize(&file_path).unwrap_err();
|
|
||||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||||
|
|
||||||
@@ -15,43 +15,45 @@ impl NoopBackend {
|
|||||||
|
|
||||||
impl VfsBackend for NoopBackend {
|
impl VfsBackend for NoopBackend {
|
||||||
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
|
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"NoopBackend doesn't do anything",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
|
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::new(
|
||||||
}
|
io::ErrorKind::Other,
|
||||||
|
"NoopBackend doesn't do anything",
|
||||||
fn exists(&mut self, _path: &Path) -> io::Result<bool> {
|
))
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
|
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::new(
|
||||||
}
|
io::ErrorKind::Other,
|
||||||
|
"NoopBackend doesn't do anything",
|
||||||
fn create_dir(&mut self, _path: &Path) -> io::Result<()> {
|
))
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"NoopBackend doesn't do anything",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"NoopBackend doesn't do anything",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
|
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::new(
|
||||||
}
|
io::ErrorKind::Other,
|
||||||
|
"NoopBackend doesn't do anything",
|
||||||
fn canonicalize(&mut self, _path: &Path) -> io::Result<PathBuf> {
|
))
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
@@ -59,16 +61,16 @@ impl VfsBackend for NoopBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"NoopBackend doesn't do anything",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
|
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::new(
|
||||||
}
|
io::ErrorKind::Other,
|
||||||
}
|
"NoopBackend doesn't do anything",
|
||||||
|
))
|
||||||
impl Default for NoopBackend {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{collections::HashSet, io};
|
|
||||||
|
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
@@ -13,7 +13,6 @@ use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
|
|||||||
pub struct StdBackend {
|
pub struct StdBackend {
|
||||||
watcher: RecommendedWatcher,
|
watcher: RecommendedWatcher,
|
||||||
watcher_receiver: Receiver<VfsEvent>,
|
watcher_receiver: Receiver<VfsEvent>,
|
||||||
watches: HashSet<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StdBackend {
|
impl StdBackend {
|
||||||
@@ -49,7 +48,6 @@ impl StdBackend {
|
|||||||
Self {
|
Self {
|
||||||
watcher,
|
watcher,
|
||||||
watcher_receiver: rx,
|
watcher_receiver: rx,
|
||||||
watches: HashSet::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,10 +61,6 @@ impl VfsBackend for StdBackend {
|
|||||||
fs_err::write(path, data)
|
fs_err::write(path, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exists(&mut self, path: &Path) -> io::Result<bool> {
|
|
||||||
std::fs::exists(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||||
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
|
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
|
||||||
let mut entries = entries?;
|
let mut entries = entries?;
|
||||||
@@ -82,14 +76,6 @@ impl VfsBackend for StdBackend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
|
|
||||||
fs_err::create_dir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
|
||||||
fs_err::create_dir_all(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||||
fs_err::remove_file(path)
|
fs_err::remove_file(path)
|
||||||
}
|
}
|
||||||
@@ -106,37 +92,19 @@ impl VfsBackend for StdBackend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
|
||||||
fs_err::canonicalize(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
self.watcher_receiver.clone()
|
self.watcher_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
||||||
if self.watches.contains(path)
|
self.watcher
|
||||||
|| path
|
.watch(path, RecursiveMode::NonRecursive)
|
||||||
.ancestors()
|
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||||
.any(|ancestor| self.watches.contains(ancestor))
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
self.watches.insert(path.to_path_buf());
|
|
||||||
self.watcher
|
|
||||||
.watch(path, RecursiveMode::Recursive)
|
|
||||||
.map_err(io::Error::other)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
||||||
self.watches.remove(path);
|
self.watcher
|
||||||
self.watcher.unwatch(path).map_err(io::Error::other)
|
.unwatch(path)
|
||||||
}
|
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for StdBackend {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ edition = "2018"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = "1.0.197"
|
serde = "1.0.99"
|
||||||
serde_yaml = "0.8.26"
|
serde_yaml = "0.8.9"
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ use serde::Serialize;
|
|||||||
/// Enables redacting any value that serializes as a string.
|
/// Enables redacting any value that serializes as a string.
|
||||||
///
|
///
|
||||||
/// Used for transforming Rojo instance IDs into something deterministic.
|
/// Used for transforming Rojo instance IDs into something deterministic.
|
||||||
#[derive(Default)]
|
|
||||||
pub struct RedactionMap {
|
pub struct RedactionMap {
|
||||||
ids: HashMap<String, usize>,
|
ids: HashMap<String, usize>,
|
||||||
last_id: usize,
|
last_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RedactionMap {
|
impl RedactionMap {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
ids: HashMap::new(),
|
||||||
|
last_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
||||||
let id = id.to_string();
|
let id = id.to_string();
|
||||||
|
|
||||||
@@ -22,12 +28,6 @@ impl RedactionMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the numeric ID that was assigned to the provided value,
|
|
||||||
/// if one exists.
|
|
||||||
pub fn get_id_for_value(&self, value: impl ToString) -> Option<usize> {
|
|
||||||
self.ids.get(&value.to_string()).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn intern(&mut self, id: impl ToString) {
|
pub fn intern(&mut self, id: impl ToString) {
|
||||||
let last_id = &mut self.last_id;
|
let last_id = &mut self.last_id;
|
||||||
|
|
||||||
|
|||||||
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 std::path::Path;
|
||||||
|
|
||||||
use globset::{Glob as InnerGlob, GlobMatcher};
|
use globset::{Glob as InnerGlob, GlobMatcher};
|
||||||
@@ -8,6 +5,8 @@ use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
|
|||||||
|
|
||||||
pub use globset::Error;
|
pub use globset::Error;
|
||||||
|
|
||||||
|
/// Wrapper around globset's Glob type that has better serialization
|
||||||
|
/// characteristics by coupling Glob and GlobMatcher into a single type.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Glob {
|
pub struct Glob {
|
||||||
inner: InnerGlob,
|
inner: InnerGlob,
|
||||||
@@ -43,8 +42,8 @@ impl Serialize for Glob {
|
|||||||
|
|
||||||
impl<'de> Deserialize<'de> for Glob {
|
impl<'de> Deserialize<'de> for Glob {
|
||||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
let glob = String::deserialize(deserializer)?;
|
let glob = <&str as Deserialize>::deserialize(deserializer)?;
|
||||||
|
|
||||||
Glob::new(&glob).map_err(D::Error::custom)
|
Glob::new(glob).map_err(D::Error::custom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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,30 +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"
|
|
||||||
},
|
|
||||||
"UploadDetails": {
|
|
||||||
"$path": "plugin/UploadDetails.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"assetId": 13916111004,
|
|
||||||
"name": "Rojo",
|
|
||||||
"description": "The plugin portion of Rojo, a tool to enable professional tooling for Roblox developers.",
|
|
||||||
"creatorId": 32644114,
|
|
||||||
"creatorType": "Group"
|
|
||||||
}
|
|
||||||
@@ -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)
|
local function defaultTableDebug(buffer, input)
|
||||||
buffer:writeRaw("{")
|
buffer:writeRaw("{")
|
||||||
|
|
||||||
for key, value in input do
|
for key, value in pairs(input) do
|
||||||
buffer:write("[{:?}] = {:?}", key, value)
|
buffer:write("[{:?}] = {:?}", key, value)
|
||||||
|
|
||||||
if next(input, key) ~= nil then
|
if next(input, key) ~= nil then
|
||||||
@@ -50,7 +50,7 @@ local function defaultTableDebugExtended(buffer, input)
|
|||||||
buffer:writeLineRaw("{")
|
buffer:writeLineRaw("{")
|
||||||
buffer:indent()
|
buffer:indent()
|
||||||
|
|
||||||
for key, value in input do
|
for key, value in pairs(input) do
|
||||||
buffer:writeLine("[{:?}] = {:#?},", key, value)
|
buffer:writeLine("[{:?}] = {:#?},", key, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
|
|||||||
elseif valueType == "table" then
|
elseif valueType == "table" then
|
||||||
local valueMeta = getmetatable(value)
|
local valueMeta = getmetatable(value)
|
||||||
|
|
||||||
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||||
-- This type implement's the metamethod we made up to line up with
|
-- This type implement's the metamethod we made up to line up with
|
||||||
-- Rust's 'Debug' trait.
|
-- Rust's 'Debug' trait.
|
||||||
|
|
||||||
@@ -242,4 +242,4 @@ return {
|
|||||||
debugOutputBuffer = debugOutputBuffer,
|
debugOutputBuffer = debugOutputBuffer,
|
||||||
fmt = fmt,
|
fmt = fmt,
|
||||||
debugify = debugify,
|
debugify = debugify,
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,12 @@ Error.__index = Error
|
|||||||
|
|
||||||
Error.Kind = {
|
Error.Kind = {
|
||||||
HttpNotEnabled = {
|
HttpNotEnabled = {
|
||||||
message = "Rojo requires HTTP access, which is not enabled.\n"
|
message = "Rojo requires HTTP access, which is not enabled.\n" ..
|
||||||
.. "Check your game settings, located in the 'Home' tab of Studio.",
|
"Check your game settings, located in the 'Home' tab of Studio.",
|
||||||
},
|
},
|
||||||
ConnectFailed = {
|
ConnectFailed = {
|
||||||
message = "Couldn't connect to the Rojo server.\n"
|
message = "Couldn't connect to the Rojo server.\n" ..
|
||||||
.. "Make sure the server is running — use 'rojo serve' to run it!",
|
"Make sure the server is running — use 'rojo serve' to run it!",
|
||||||
},
|
},
|
||||||
Timeout = {
|
Timeout = {
|
||||||
message = "HTTP request timed out.",
|
message = "HTTP request timed out.",
|
||||||
@@ -63,13 +63,4 @@ function Error.fromRobloxErrorString(message)
|
|||||||
return Error.new(Error.Kind.Unknown, message)
|
return Error.new(Error.Kind.Unknown, message)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Error.fromResponse(response)
|
|
||||||
local lower = (response.body or ""):lower()
|
|
||||||
if response.code == 408 or response.code == 504 or lower:find("timed? ?out") then
|
|
||||||
return Error.new(Error.Kind.Timeout)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Error.new(Error.Kind.Unknown, string.format("%s: %s", tostring(response.code), tostring(response.body)))
|
|
||||||
end
|
|
||||||
|
|
||||||
return Error
|
return Error
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
local msgpack = require(script.Parent.Parent.msgpack)
|
|
||||||
|
|
||||||
local stringTemplate = [[
|
local stringTemplate = [[
|
||||||
Http.Response {
|
Http.Response {
|
||||||
code: %d
|
code: %d
|
||||||
@@ -33,8 +31,4 @@ function Response:json()
|
|||||||
return HttpService:JSONDecode(self.body)
|
return HttpService:JSONDecode(self.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Response:msgpack()
|
return Response
|
||||||
return msgpack.decode(self.body)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Response
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
local Log = require(script.Parent.Log)
|
|
||||||
local msgpack = require(script.Parent.msgpack)
|
|
||||||
local Promise = require(script.Parent.Promise)
|
local Promise = require(script.Parent.Promise)
|
||||||
|
local Log = require(script.Parent.Log)
|
||||||
|
|
||||||
local HttpError = require(script.Error)
|
local HttpError = require(script.Error)
|
||||||
local HttpResponse = require(script.Response)
|
local HttpResponse = require(script.Response)
|
||||||
@@ -31,13 +30,8 @@ local function performRequest(requestParams)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
Log.trace("Request {} success, response {:#?}", requestId, response)
|
Log.trace("Request {} success, status code {}", requestId, response.StatusCode)
|
||||||
local httpResponse = HttpResponse.fromRobloxResponse(response)
|
resolve(HttpResponse.fromRobloxResponse(response))
|
||||||
if httpResponse:isSuccess() then
|
|
||||||
resolve(httpResponse)
|
|
||||||
else
|
|
||||||
reject(HttpError.fromResponse(httpResponse))
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
Log.trace("Request {} failure: {:?}", requestId, response)
|
Log.trace("Request {} failure: {:?}", requestId, response)
|
||||||
reject(HttpError.fromRobloxErrorString(response))
|
reject(HttpError.fromRobloxErrorString(response))
|
||||||
@@ -69,12 +63,4 @@ function Http.jsonDecode(source)
|
|||||||
return HttpService:JSONDecode(source)
|
return HttpService:JSONDecode(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Http.msgpackEncode(object)
|
return Http
|
||||||
return msgpack.encode(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Http.msgpackDecode(source)
|
|
||||||
return msgpack.decode(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Http
|
|
||||||
@@ -2,4 +2,4 @@ return function()
|
|||||||
it("should load", function()
|
it("should load", function()
|
||||||
require(script.Parent)
|
require(script.Parent)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -57,4 +57,4 @@ function Log.error(template, ...)
|
|||||||
error(Fmt.fmt(template, ...))
|
error(Fmt.fmt(template, ...))
|
||||||
end
|
end
|
||||||
|
|
||||||
return Log
|
return Log
|
||||||
@@ -2,4 +2,4 @@ return function()
|
|||||||
it("should load", function()
|
it("should load", function()
|
||||||
require(script.Parent)
|
require(script.Parent)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
1
plugin/modules/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
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
local ALL_AXES = { "X", "Y", "Z" }
|
local ALL_AXES = {"X", "Y", "Z"}
|
||||||
local ALL_FACES = { "Right", "Top", "Back", "Left", "Bottom", "Front" }
|
local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
|
||||||
|
|
||||||
local EncodedValue = {}
|
|
||||||
|
|
||||||
local types
|
local types
|
||||||
types = {
|
types = {
|
||||||
Attributes = {
|
|
||||||
fromPod = function(pod)
|
|
||||||
local output = {}
|
|
||||||
|
|
||||||
for key, value in pairs(pod) do
|
|
||||||
local ok, result = EncodedValue.decode(value)
|
|
||||||
|
|
||||||
if ok then
|
|
||||||
output[key] = result
|
|
||||||
else
|
|
||||||
local warning = ("Could not decode attribute value of type %q: %s"):format(
|
|
||||||
typeof(value),
|
|
||||||
tostring(result)
|
|
||||||
)
|
|
||||||
warn(warning)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return output
|
|
||||||
end,
|
|
||||||
toPod = function(roblox)
|
|
||||||
local output = {}
|
|
||||||
|
|
||||||
for key, value in pairs(roblox) do
|
|
||||||
local ok, result = EncodedValue.encodeNaive(value)
|
|
||||||
|
|
||||||
if ok then
|
|
||||||
output[key] = result
|
|
||||||
else
|
|
||||||
local warning = ("Could not encode attribute value of type %q: %s"):format(
|
|
||||||
typeof(value),
|
|
||||||
tostring(result)
|
|
||||||
)
|
|
||||||
warn(warning)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return output
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
|
|
||||||
Axes = {
|
Axes = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
local axes = {}
|
local axes = {}
|
||||||
@@ -117,7 +74,6 @@ types = {
|
|||||||
local pos = pod.position
|
local pos = pod.position
|
||||||
local orient = pod.orientation
|
local orient = pod.orientation
|
||||||
|
|
||||||
--stylua: ignore
|
|
||||||
return CFrame.new(
|
return CFrame.new(
|
||||||
pos[1], pos[2], pos[3],
|
pos[1], pos[2], pos[3],
|
||||||
orient[1][1], orient[1][2], orient[1][3],
|
orient[1][1], orient[1][2], orient[1][3],
|
||||||
@@ -127,14 +83,17 @@ types = {
|
|||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = roblox:GetComponents()
|
local x, y, z,
|
||||||
|
r00, r01, r02,
|
||||||
|
r10, r11, r12,
|
||||||
|
r20, r21, r22 = roblox:GetComponents()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position = { x, y, z },
|
position = {x, y, z},
|
||||||
orientation = {
|
orientation = {
|
||||||
{ r00, r01, r02 },
|
{r00, r01, r02},
|
||||||
{ r10, r11, r12 },
|
{r10, r11, r12},
|
||||||
{ r20, r21, r22 },
|
{r20, r21, r22},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
@@ -144,7 +103,7 @@ types = {
|
|||||||
fromPod = unpackDecoder(Color3.new),
|
fromPod = unpackDecoder(Color3.new),
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
return { roblox.r, roblox.g, roblox.b }
|
return {roblox.r, roblox.g, roblox.b}
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -165,7 +124,10 @@ types = {
|
|||||||
local keypoints = {}
|
local keypoints = {}
|
||||||
|
|
||||||
for index, keypoint in ipairs(pod.keypoints) do
|
for index, keypoint in ipairs(pod.keypoints) do
|
||||||
keypoints[index] = ColorSequenceKeypoint.new(keypoint.time, types.Color3.fromPod(keypoint.color))
|
keypoints[index] = ColorSequenceKeypoint.new(
|
||||||
|
keypoint.time,
|
||||||
|
types.Color3.fromPod(keypoint.color)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ColorSequence.new(keypoints)
|
return ColorSequence.new(keypoints)
|
||||||
@@ -188,38 +150,6 @@ types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Content = {
|
Content = {
|
||||||
fromPod = function(pod): Content
|
|
||||||
if type(pod) == "string" then
|
|
||||||
if pod == "None" then
|
|
||||||
return Content.none
|
|
||||||
else
|
|
||||||
error(`unexpected Content value '{pod}'`)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local ty, value = next(pod)
|
|
||||||
if ty == "Uri" then
|
|
||||||
return Content.fromUri(value)
|
|
||||||
elseif ty == "Object" then
|
|
||||||
error("Object deserializing is not currently implemented")
|
|
||||||
else
|
|
||||||
error(`Unknown Content type '{ty}' (could not deserialize)`)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
toPod = function(roblox: Content)
|
|
||||||
if roblox.SourceType == Enum.ContentSourceType.None then
|
|
||||||
return "None"
|
|
||||||
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
|
|
||||||
return { Uri = roblox.Uri }
|
|
||||||
elseif roblox.SourceType == Enum.ContentSourceType.Object then
|
|
||||||
error("Object serializing is not currently implemented")
|
|
||||||
else
|
|
||||||
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
|
|
||||||
ContentId = {
|
|
||||||
fromPod = identity,
|
fromPod = identity,
|
||||||
toPod = identity,
|
toPod = identity,
|
||||||
},
|
},
|
||||||
@@ -237,19 +167,6 @@ types = {
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
EnumItem = {
|
|
||||||
fromPod = function(pod)
|
|
||||||
return Enum[pod.type]:FromValue(pod.value)
|
|
||||||
end,
|
|
||||||
|
|
||||||
toPod = function(roblox)
|
|
||||||
return {
|
|
||||||
type = tostring(roblox.EnumType),
|
|
||||||
value = roblox.Value,
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
|
|
||||||
Faces = {
|
Faces = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
local faces = {}
|
local faces = {}
|
||||||
@@ -284,23 +201,6 @@ types = {
|
|||||||
toPod = serializeFloat,
|
toPod = serializeFloat,
|
||||||
},
|
},
|
||||||
|
|
||||||
Font = {
|
|
||||||
fromPod = function(pod)
|
|
||||||
return Font.new(
|
|
||||||
pod.family,
|
|
||||||
if pod.weight ~= nil then Enum.FontWeight[pod.weight] else nil,
|
|
||||||
if pod.style ~= nil then Enum.FontStyle[pod.style] else nil
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
toPod = function(roblox)
|
|
||||||
return {
|
|
||||||
family = roblox.Family,
|
|
||||||
weight = roblox.Weight.Name,
|
|
||||||
style = roblox.Style.Name,
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
|
|
||||||
Int32 = {
|
Int32 = {
|
||||||
fromPod = identity,
|
fromPod = identity,
|
||||||
toPod = identity,
|
toPod = identity,
|
||||||
@@ -311,32 +211,11 @@ types = {
|
|||||||
toPod = identity,
|
toPod = identity,
|
||||||
},
|
},
|
||||||
|
|
||||||
MaterialColors = {
|
|
||||||
fromPod = function(pod: { [string]: { number } })
|
|
||||||
local real = {}
|
|
||||||
for name, color in pod do
|
|
||||||
real[Enum.Material[name]] = Color3.fromRGB(color[1], color[2], color[3])
|
|
||||||
end
|
|
||||||
return real
|
|
||||||
end,
|
|
||||||
toPod = function(roblox: { [Enum.Material]: Color3 })
|
|
||||||
local pod = {}
|
|
||||||
for material, color in roblox do
|
|
||||||
pod[material.Name] = {
|
|
||||||
math.round(math.clamp(color.R, 0, 1) * 255),
|
|
||||||
math.round(math.clamp(color.G, 0, 1) * 255),
|
|
||||||
math.round(math.clamp(color.B, 0, 1) * 255),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
return pod
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
|
|
||||||
NumberRange = {
|
NumberRange = {
|
||||||
fromPod = unpackDecoder(NumberRange.new),
|
fromPod = unpackDecoder(NumberRange.new),
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
return { roblox.Min, roblox.Max }
|
return {roblox.Min, roblox.Max}
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -345,12 +224,11 @@ types = {
|
|||||||
local keypoints = {}
|
local keypoints = {}
|
||||||
|
|
||||||
for index, keypoint in ipairs(pod.keypoints) do
|
for index, keypoint in ipairs(pod.keypoints) do
|
||||||
-- TODO: Add a test for NaN or Infinity values and envelopes
|
keypoints[index] = NumberSequenceKeypoint.new(
|
||||||
-- Right now it isn't possible because it'd fail the roundtrip.
|
keypoint.time,
|
||||||
-- It's more important that it works right now, though.
|
keypoint.value,
|
||||||
local value = keypoint.value or 0
|
keypoint.envelope
|
||||||
local envelope = keypoint.envelope or 0
|
)
|
||||||
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return NumberSequence.new(keypoints)
|
return NumberSequence.new(keypoints)
|
||||||
@@ -378,26 +256,13 @@ types = {
|
|||||||
if pod == "Default" then
|
if pod == "Default" then
|
||||||
return nil
|
return nil
|
||||||
else
|
else
|
||||||
-- Passing `nil` instead of not passing anything gives
|
return PhysicalProperties.new(
|
||||||
-- different results, so we have to branch here.
|
pod.density,
|
||||||
if pod.acousticAbsorption then
|
pod.friction,
|
||||||
return (PhysicalProperties.new :: any)(
|
pod.elasticity,
|
||||||
pod.density,
|
pod.frictionWeight,
|
||||||
pod.friction,
|
pod.elasticityWeight
|
||||||
pod.elasticity,
|
)
|
||||||
pod.frictionWeight,
|
|
||||||
pod.elasticityWeight,
|
|
||||||
pod.acousticAbsorption
|
|
||||||
)
|
|
||||||
else
|
|
||||||
return PhysicalProperties.new(
|
|
||||||
pod.density,
|
|
||||||
pod.friction,
|
|
||||||
pod.elasticity,
|
|
||||||
pod.frictionWeight,
|
|
||||||
pod.elasticityWeight
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
@@ -411,7 +276,6 @@ types = {
|
|||||||
elasticity = roblox.Elasticity,
|
elasticity = roblox.Elasticity,
|
||||||
frictionWeight = roblox.FrictionWeight,
|
frictionWeight = roblox.FrictionWeight,
|
||||||
elasticityWeight = roblox.ElasticityWeight,
|
elasticityWeight = roblox.ElasticityWeight,
|
||||||
acousticAbsorption = roblox.AcousticAbsorption,
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
@@ -419,7 +283,10 @@ types = {
|
|||||||
|
|
||||||
Ray = {
|
Ray = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return Ray.new(types.Vector3.fromPod(pod.origin), types.Vector3.fromPod(pod.direction))
|
return Ray.new(
|
||||||
|
types.Vector3.fromPod(pod.origin),
|
||||||
|
types.Vector3.fromPod(pod.direction)
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -432,7 +299,10 @@ types = {
|
|||||||
|
|
||||||
Rect = {
|
Rect = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return Rect.new(types.Vector2.fromPod(pod[1]), types.Vector2.fromPod(pod[2]))
|
return Rect.new(
|
||||||
|
types.Vector2.fromPod(pod[1]),
|
||||||
|
types.Vector2.fromPod(pod[2])
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -444,28 +314,31 @@ types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Ref = {
|
Ref = {
|
||||||
fromPod = function(_)
|
fromPod = function(_pod)
|
||||||
error("Ref cannot be decoded on its own")
|
error("Ref cannot be decoded on its own")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(_)
|
toPod = function(_roblox)
|
||||||
error("Ref can not be encoded on its own")
|
error("Ref can not be encoded on its own")
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
Region3 = {
|
Region3 = {
|
||||||
fromPod = function(_)
|
fromPod = function(pod)
|
||||||
error("Region3 is not implemented")
|
error("Region3 is not implemented")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(_)
|
toPod = function(roblox)
|
||||||
error("Region3 is not implemented")
|
error("Region3 is not implemented")
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
Region3int16 = {
|
Region3int16 = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return Region3int16.new(types.Vector3int16.fromPod(pod[1]), types.Vector3int16.fromPod(pod[2]))
|
return Region3int16.new(
|
||||||
|
types.Vector3int16.fromPod(pod[1]),
|
||||||
|
types.Vector3int16.fromPod(pod[2])
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -477,11 +350,11 @@ types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
SharedString = {
|
SharedString = {
|
||||||
fromPod = function(_pod)
|
fromPod = function(pod)
|
||||||
error("SharedString is not supported")
|
error("SharedString is not supported")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(_roblox)
|
toPod = function(roblox)
|
||||||
error("SharedString is not supported")
|
error("SharedString is not supported")
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
@@ -495,13 +368,16 @@ types = {
|
|||||||
fromPod = unpackDecoder(UDim.new),
|
fromPod = unpackDecoder(UDim.new),
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
return { roblox.Scale, roblox.Offset }
|
return {roblox.Scale, roblox.Offset}
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
UDim2 = {
|
UDim2 = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return UDim2.new(types.UDim.fromPod(pod[1]), types.UDim.fromPod(pod[2]))
|
return UDim2.new(
|
||||||
|
types.UDim.fromPod(pod[1]),
|
||||||
|
types.UDim.fromPod(pod[2])
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -532,7 +408,7 @@ types = {
|
|||||||
fromPod = unpackDecoder(Vector2int16.new),
|
fromPod = unpackDecoder(Vector2int16.new),
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
return { roblox.X, roblox.Y }
|
return {roblox.X, roblox.Y}
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -552,37 +428,16 @@ types = {
|
|||||||
fromPod = unpackDecoder(Vector3int16.new),
|
fromPod = unpackDecoder(Vector3int16.new),
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
return { roblox.X, roblox.Y, roblox.Z }
|
return {roblox.X, roblox.Y, roblox.Z}
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
types.OptionalCFrame = {
|
local EncodedValue = {}
|
||||||
fromPod = function(pod)
|
|
||||||
if pod == nil then
|
|
||||||
return nil
|
|
||||||
else
|
|
||||||
return types.CFrame.fromPod(pod)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
toPod = function(roblox)
|
|
||||||
if roblox == nil then
|
|
||||||
return nil
|
|
||||||
else
|
|
||||||
return types.CFrame.toPod(roblox)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
function EncodedValue.decode(encodedValue)
|
function EncodedValue.decode(encodedValue)
|
||||||
local ty, value = next(encodedValue)
|
local ty, value = next(encodedValue)
|
||||||
|
|
||||||
if ty == nil then
|
|
||||||
-- If the encoded pair is empty, assume it is an unoccupied optional value
|
|
||||||
return true, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local typeImpl = types[ty]
|
local typeImpl = types[ty]
|
||||||
if typeImpl == nil then
|
if typeImpl == nil then
|
||||||
return false, "Couldn't decode value " .. tostring(ty)
|
return false, "Couldn't decode value " .. tostring(ty)
|
||||||
@@ -604,19 +459,4 @@ function EncodedValue.encode(rbxValue, propertyType)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local propertyTypeRenames = {
|
|
||||||
number = "Float64",
|
|
||||||
boolean = "Bool",
|
|
||||||
string = "String",
|
|
||||||
}
|
|
||||||
|
|
||||||
function EncodedValue.encodeNaive(rbxValue)
|
|
||||||
local propertyType = typeof(rbxValue)
|
|
||||||
if propertyTypeRenames[propertyType] ~= nil then
|
|
||||||
propertyType = propertyTypeRenames[propertyType]
|
|
||||||
end
|
|
||||||
|
|
||||||
return EncodedValue.encode(rbxValue, propertyType)
|
|
||||||
end
|
|
||||||
|
|
||||||
return EncodedValue
|
return EncodedValue
|
||||||
|
|||||||
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",
|
UnknownProperty = "UnknownProperty",
|
||||||
PropertyNotReadable = "PropertyNotReadable",
|
PropertyNotReadable = "PropertyNotReadable",
|
||||||
PropertyNotWritable = "PropertyNotWritable",
|
PropertyNotWritable = "PropertyNotWritable",
|
||||||
CannotParseBinaryString = "CannotParseBinaryString",
|
|
||||||
Roblox = "Roblox",
|
Roblox = "Roblox",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,4 +25,4 @@ function Error:__tostring()
|
|||||||
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
|
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
|
||||||
end
|
end
|
||||||
|
|
||||||
return Error
|
return Error
|
||||||
@@ -53,11 +53,6 @@ function PropertyDescriptor:read(instance)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if self.scriptability == "Custom" then
|
if self.scriptability == "Custom" then
|
||||||
if customProperties[self.className] == nil then
|
|
||||||
local fullName = ("%s.%s"):format(instance.className, self.name)
|
|
||||||
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
|
|
||||||
end
|
|
||||||
|
|
||||||
local interface = customProperties[self.className][self.name]
|
local interface = customProperties[self.className][self.name]
|
||||||
|
|
||||||
return interface.read(instance, self.name)
|
return interface.read(instance, self.name)
|
||||||
@@ -84,11 +79,6 @@ function PropertyDescriptor:write(instance, value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if self.scriptability == "Custom" then
|
if self.scriptability == "Custom" then
|
||||||
if customProperties[self.className] == nil then
|
|
||||||
local fullName = ("%s.%s"):format(instance.className, self.name)
|
|
||||||
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
|
|
||||||
end
|
|
||||||
|
|
||||||
local interface = customProperties[self.className][self.name]
|
local interface = customProperties[self.className][self.name]
|
||||||
|
|
||||||
return interface.write(instance, self.name, value)
|
return interface.write(instance, self.name, value)
|
||||||
|
|||||||
@@ -1,79 +1,4 @@
|
|||||||
{
|
{
|
||||||
"Attributes": {
|
|
||||||
"value": {
|
|
||||||
"Attributes": {
|
|
||||||
"TestBool": {
|
|
||||||
"Bool": true
|
|
||||||
},
|
|
||||||
"TestBrickColor": {
|
|
||||||
"BrickColor": 24
|
|
||||||
},
|
|
||||||
"TestColor3": {
|
|
||||||
"Color3": [
|
|
||||||
1.0,
|
|
||||||
0.5,
|
|
||||||
0.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"TestEnumItem": {
|
|
||||||
"EnumItem": {
|
|
||||||
"type": "Material",
|
|
||||||
"value": 256
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TestNumber": {
|
|
||||||
"Float64": 1337.0
|
|
||||||
},
|
|
||||||
"TestRect": {
|
|
||||||
"Rect": [
|
|
||||||
[
|
|
||||||
1.0,
|
|
||||||
2.0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.0,
|
|
||||||
4.0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"TestString": {
|
|
||||||
"String": "Test"
|
|
||||||
},
|
|
||||||
"TestUDim": {
|
|
||||||
"UDim": [
|
|
||||||
1.0,
|
|
||||||
2
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"TestUDim2": {
|
|
||||||
"UDim2": [
|
|
||||||
[
|
|
||||||
1.0,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.0,
|
|
||||||
4
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"TestVector2": {
|
|
||||||
"Vector2": [
|
|
||||||
1.0,
|
|
||||||
2.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"TestVector3": {
|
|
||||||
"Vector3": [
|
|
||||||
1.0,
|
|
||||||
2.0,
|
|
||||||
3.0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ty": "Attributes"
|
|
||||||
},
|
|
||||||
"Axes": {
|
"Axes": {
|
||||||
"value": {
|
"value": {
|
||||||
"Axes": [
|
"Axes": [
|
||||||
@@ -176,23 +101,9 @@
|
|||||||
},
|
},
|
||||||
"ty": "ColorSequence"
|
"ty": "ColorSequence"
|
||||||
},
|
},
|
||||||
"ContentId": {
|
"Content": {
|
||||||
"value": {
|
"value": {
|
||||||
"ContentId": "rbxassetid://12345"
|
"Content": "rbxassetid://12345"
|
||||||
},
|
|
||||||
"ty": "ContentId"
|
|
||||||
},
|
|
||||||
"Content_None": {
|
|
||||||
"value": {
|
|
||||||
"Content": "None"
|
|
||||||
},
|
|
||||||
"ty": "Content"
|
|
||||||
},
|
|
||||||
"Content_Uri": {
|
|
||||||
"value": {
|
|
||||||
"Content": {
|
|
||||||
"Uri": "rbxasset://abc/123.rojo"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"ty": "Content"
|
"ty": "Content"
|
||||||
},
|
},
|
||||||
@@ -202,15 +113,6 @@
|
|||||||
},
|
},
|
||||||
"ty": "Enum"
|
"ty": "Enum"
|
||||||
},
|
},
|
||||||
"EnumItem": {
|
|
||||||
"value": {
|
|
||||||
"EnumItem": {
|
|
||||||
"type": "Material",
|
|
||||||
"value": 256
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ty": "EnumItem"
|
|
||||||
},
|
|
||||||
"Faces": {
|
"Faces": {
|
||||||
"value": {
|
"value": {
|
||||||
"Faces": [
|
"Faces": [
|
||||||
@@ -236,17 +138,6 @@
|
|||||||
},
|
},
|
||||||
"ty": "Float64"
|
"ty": "Float64"
|
||||||
},
|
},
|
||||||
"Font": {
|
|
||||||
"value": {
|
|
||||||
"Font": {
|
|
||||||
"family": "rbxasset://fonts/families/SourceSansPro.json",
|
|
||||||
"weight": "Regular",
|
|
||||||
"style": "Normal",
|
|
||||||
"cachedFaceId": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ty": "Font"
|
|
||||||
},
|
|
||||||
"Int32": {
|
"Int32": {
|
||||||
"value": {
|
"value": {
|
||||||
"Int32": 6014
|
"Int32": 6014
|
||||||
@@ -259,118 +150,6 @@
|
|||||||
},
|
},
|
||||||
"ty": "Int64"
|
"ty": "Int64"
|
||||||
},
|
},
|
||||||
"MaterialColors": {
|
|
||||||
"value": {
|
|
||||||
"MaterialColors": {
|
|
||||||
"Grass": [
|
|
||||||
106,
|
|
||||||
127,
|
|
||||||
63
|
|
||||||
],
|
|
||||||
"Slate": [
|
|
||||||
63,
|
|
||||||
127,
|
|
||||||
107
|
|
||||||
],
|
|
||||||
"Concrete": [
|
|
||||||
127,
|
|
||||||
102,
|
|
||||||
63
|
|
||||||
],
|
|
||||||
"Brick": [
|
|
||||||
138,
|
|
||||||
86,
|
|
||||||
62
|
|
||||||
],
|
|
||||||
"Sand": [
|
|
||||||
143,
|
|
||||||
126,
|
|
||||||
95
|
|
||||||
],
|
|
||||||
"WoodPlanks": [
|
|
||||||
139,
|
|
||||||
109,
|
|
||||||
79
|
|
||||||
],
|
|
||||||
"Rock": [
|
|
||||||
102,
|
|
||||||
108,
|
|
||||||
111
|
|
||||||
],
|
|
||||||
"Glacier": [
|
|
||||||
101,
|
|
||||||
176,
|
|
||||||
234
|
|
||||||
],
|
|
||||||
"Snow": [
|
|
||||||
195,
|
|
||||||
199,
|
|
||||||
218
|
|
||||||
],
|
|
||||||
"Sandstone": [
|
|
||||||
137,
|
|
||||||
90,
|
|
||||||
71
|
|
||||||
],
|
|
||||||
"Mud": [
|
|
||||||
58,
|
|
||||||
46,
|
|
||||||
36
|
|
||||||
],
|
|
||||||
"Basalt": [
|
|
||||||
30,
|
|
||||||
30,
|
|
||||||
37
|
|
||||||
],
|
|
||||||
"Ground": [
|
|
||||||
102,
|
|
||||||
92,
|
|
||||||
59
|
|
||||||
],
|
|
||||||
"CrackedLava": [
|
|
||||||
232,
|
|
||||||
156,
|
|
||||||
74
|
|
||||||
],
|
|
||||||
"Asphalt": [
|
|
||||||
115,
|
|
||||||
123,
|
|
||||||
107
|
|
||||||
],
|
|
||||||
"Cobblestone": [
|
|
||||||
132,
|
|
||||||
123,
|
|
||||||
90
|
|
||||||
],
|
|
||||||
"Ice": [
|
|
||||||
129,
|
|
||||||
194,
|
|
||||||
224
|
|
||||||
],
|
|
||||||
"LeafyGrass": [
|
|
||||||
115,
|
|
||||||
132,
|
|
||||||
74
|
|
||||||
],
|
|
||||||
"Salt": [
|
|
||||||
198,
|
|
||||||
189,
|
|
||||||
181
|
|
||||||
],
|
|
||||||
"Limestone": [
|
|
||||||
206,
|
|
||||||
173,
|
|
||||||
148
|
|
||||||
],
|
|
||||||
"Pavement": [
|
|
||||||
148,
|
|
||||||
148,
|
|
||||||
140
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ty": "MaterialColors"
|
|
||||||
},
|
|
||||||
"NumberRange": {
|
"NumberRange": {
|
||||||
"value": {
|
"value": {
|
||||||
"NumberRange": [
|
"NumberRange": [
|
||||||
@@ -399,41 +178,6 @@
|
|||||||
},
|
},
|
||||||
"ty": "NumberSequence"
|
"ty": "NumberSequence"
|
||||||
},
|
},
|
||||||
"OptionalCFrame-None": {
|
|
||||||
"value": {
|
|
||||||
"OptionalCFrame": null
|
|
||||||
},
|
|
||||||
"ty": "OptionalCFrame"
|
|
||||||
},
|
|
||||||
"OptionalCFrame-Some": {
|
|
||||||
"value": {
|
|
||||||
"OptionalCFrame": {
|
|
||||||
"position": [
|
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"orientation": [
|
|
||||||
[
|
|
||||||
1.0,
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.0,
|
|
||||||
1.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ty": "OptionalCFrame"
|
|
||||||
},
|
|
||||||
"PhysicalProperties-Custom": {
|
"PhysicalProperties-Custom": {
|
||||||
"value": {
|
"value": {
|
||||||
"PhysicalProperties": {
|
"PhysicalProperties": {
|
||||||
@@ -441,8 +185,7 @@
|
|||||||
"friction": 1.0,
|
"friction": 1.0,
|
||||||
"elasticity": 0.0,
|
"elasticity": 0.0,
|
||||||
"frictionWeight": 50.0,
|
"frictionWeight": 50.0,
|
||||||
"elasticityWeight": 25.0,
|
"elasticityWeight": 25.0
|
||||||
"acousticAbsorption": 0.15625
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ty": "PhysicalProperties"
|
"ty": "PhysicalProperties"
|
||||||
|
|||||||
@@ -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 {
|
return {
|
||||||
decode = function(input: string)
|
decode = decodeBase64,
|
||||||
return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input)))
|
encode = encodeBase64,
|
||||||
end,
|
}
|
||||||
encode = function(input: string)
|
|
||||||
return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input)))
|
|
||||||
end,
|
|
||||||
}
|
|
||||||