Compare commits

..

47 Commits

Author SHA1 Message Date
Lucien Greathouse
88d2d1f193 Stub out a command sorta like rostar unpack 2022-06-10 03:15:44 -04:00
Lucien Greathouse
2e7c4b6dff Update Changelog 2022-06-10 02:15:07 -04:00
Lucien Greathouse
e5dbee1073 Defer application of init.meta.json until after init Lua files. (#549)
Fixes #546.
2022-06-09 21:42:37 -04:00
Lucien Greathouse
c06463b61d Update CHANGELOG 2022-06-05 17:48:17 -04:00
Lucien Greathouse
10341e3776 Major Performance Improvements (#548)
* Use WeakDom::into_raw for faster snapshot generation from models

* Make compute_patch_set take snapshots by value

* Stop deferring property application in apply_patch_set

* Use InstanceBuilder::empty to avoid extra name allocations

* Git dependencies, skip dropping ServeSession

* Use std::mem::forget instead of ManuallyDrop

* Switch to latest rbx-dom crates.io dependencies

* Update other dependencies
2022-06-05 17:47:31 -04:00
Lucien Greathouse
824cdc5dcd Annotate snapshot_rbxm for profiling 2022-05-27 18:13:07 -04:00
Lucien Greathouse
7aa7a35aa5 Add profiling info and optional profiling with Tracy 2022-05-27 03:08:54 -04:00
Lucien Greathouse
79b57b3359 Move memofs and rojo-insta-ext into crates folder 2022-05-26 04:23:44 -04:00
Lucien Greathouse
c7aeffe586 Switch from structopt to clap 2022-05-26 04:19:51 -04:00
Lucien Greathouse
79c02f2457 Delete old bin folder and update foreman.toml 2022-05-26 04:13:50 -04:00
Lucien Greathouse
b9ed68fa9e Release v7.1.1 2022-05-26 02:53:20 -04:00
Lucien Greathouse
6c6d6c9c8d Add .github/FUNDING.yml 2022-05-26 02:28:57 -04:00
Lucien Greathouse
e169d7be68 New release workflow (#547)
* Port release workflow from Aftman to test

* Checkout submodules in plugin build step

* ...and build with submodules for other builds too

* Fix ci.yml; we use master branch still

* CI with submodules too
2022-05-25 22:26:22 -04:00
Lucien Greathouse
192fd7d4dd New and improved CI pipeline 2022-05-25 18:53:08 -04:00
Lucien Greathouse
1f1193e857 Remove unused lazy_static 2022-05-25 18:48:57 -04:00
boatbomber
0a412ade88 Remove duplicate PluginSettings.StudioProvider (#545) 2022-05-25 18:48:10 -04:00
Filip Tibell
3cef2fe9aa Fix sourcemap command not stripping paths correctly (#544)
* Fix sourcemap command not stripping paths correctly

* Use ServeSession to get the proper root dir to strip for sourcemap
2022-05-23 15:19:30 -04:00
Lucien Greathouse
18e53f06fe Remove unused dependencies and dead code warnings 2022-05-22 19:20:41 -04:00
Lucien Greathouse
eaac539087 Update to reqwest 0.11.10 2022-05-22 19:16:43 -04:00
Lucien Greathouse
57005c4fd5 Update uuid and winreg 2022-05-22 19:13:11 -04:00
Lucien Greathouse
ea58999a2a Update to pretty_assertions 1.2.1 2022-05-22 19:12:33 -04:00
Lucien Greathouse
a5a69fd9fc Release v7.1.0 2022-05-22 18:53:45 -04:00
boatbomber
f1d0f1c1c9 Bugfix: PluginAction spam causing errors (#541)
* Use session's state instead of existence to determine action

* Retain host/port text

* Use bindings instead of text/ref tunneling

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-05-22 18:47:11 -04:00
boatbomber
83492d7495 PluginActions for connecting/disconnecting a session (#537)
* Create plugin action component

* Add plugin action for session start/end

* Add output for connection status change

* Move host & port refs to App level so keybind can access them

* Use passed function directly

* Improve the action text clarity

* Add actions for single action

* Add to changelog

* Explicitly return nil

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>

* Change log level to info

* Refactor startSession to contain the logic

* Formatting

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-05-02 17:49:31 -04:00
boatbomber
10abc2254a Add changing toolbar icon to indicate state (#538)
* Add changing toolbar icon

* Return to default icon after closing error

* Update changelog

* Add assets

* Improved link icon

* Upload new icons

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-05-02 17:28:18 -04:00
Lucien Greathouse
5d5536a95e Update CHANGELOG 2022-04-19 18:45:09 -04:00
James Onnen
fe81e55925 Add support for optional paths (#472)
* Add PathNode with optional fields to project. This allows a path to be defined either as `"$path": "src"` or `"$path": { "optional": "src" }`

* Make $path truly optional

* Prevent rojo from erroring if no project node is resolved

* Use match instead of if-statement

* Add end-to-end tests (credit to MobiusCraftFlip for initial scenario)

* Pass option with ref inside instead of reference to option

* Empty commit to restart GitHub Actions

* Simplify build test

* Minimize serve test: it fails

* Simplify serve test even more

* Ignore failing serve test

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-04-19 18:43:47 -04:00
Micah
654690d73e Add testez std (#535) 2022-04-19 17:45:55 -04:00
Filip Tibell
256aba4bc1 Implement sourcemap CLI command (#530)
* Initial implementation of sourcemap CLI command

* Update src/cli/sourcemap.rs

Co-authored-by: JohnnyMorganz <johnnymorganz@outlook.com>

* Update src/cli/sourcemap.rs

Co-authored-by: JohnnyMorganz <johnnymorganz@outlook.com>

* Tidy up sourcemap command

* Update CHANGELOG

Co-authored-by: JohnnyMorganz <johnnymorganz@outlook.com>
Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-04-19 17:45:29 -04:00
Ashton Miller
49f8845105 Add ability to specify address in default.project.json (#507)
* Allow for setting the default port in project json

set as
```json
"serveAddress": "0.0.0.0"
```

* Update CHANGELOG.md

* cargo fmt

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-04-19 16:54:03 -04:00
Ashton Miller
12370846b4 Support the new Open Cloud API (#504)
* Add support for the new Open Cloud API

* Cleanup Open Cloud variables

* Avoid cloning buffer for do_upload_open_cloud

* Satisfy cargo fmt

* Actually correct cargo fmt

Apparently my earlier fix did not fix everything.

* Update CHANGELOG.md

* Update CHANGELOG.md

Forgot to add the link to issue #486 in the previous commit :/

* Cleanup & improve code for open cloud api

* Commit to force GH Actions to run (?)

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-04-19 16:02:40 -04:00
Lucien Greathouse
07637dfe96 Release v7.0.0 2021-12-10 19:37:39 -05:00
Lucien Greathouse
f389a4a1db Update rbx_dom_lua 2021-11-26 17:31:12 -05:00
Lucien Greathouse
af077c796c Update dependencies 2021-11-26 17:28:13 -05:00
Lucien Greathouse
1c319f2fa8 Promote weldconstraint test project to a build test 2021-11-26 16:36:55 -05:00
Lucien Greathouse
e8afa03f7b Fix most test output (but not termcolor) 2021-11-22 13:59:12 -05:00
Lucien Greathouse
9b22545842 Add tests for current file naming with regards to project name field 2021-11-22 13:22:16 -05:00
Lucien Greathouse
adc733d25c Update changelog 2021-11-20 18:14:17 -05:00
Lucien Greathouse
6896257647 Bump MSRV to 1.55.0 2021-11-20 18:07:24 -05:00
Lucien Greathouse
1d9845a6cb Update dependencies 2021-11-20 18:06:41 -05:00
Blake Mealey
8461339e9a Add note for git submodules (#495) 2021-11-20 17:53:22 -05:00
Umbreon
9904d94e4c Remember sync connection settings. (#500) 2021-11-20 17:51:38 -05:00
Lucien Greathouse
da25c80d0b Add support for CFrame shorthand. Fixes #430. 2021-11-20 17:50:40 -05:00
Lucien Greathouse
5fa63733fd Factor out property filtering code to simplify web server 2021-11-20 17:38:36 -05:00
Lucien Greathouse
8b54bf0ba1 Improve error when file is not found 2021-11-20 17:15:58 -05:00
Lucien Greathouse
173dc12cb3 Improve warning and debug output in plugin 2021-11-20 17:05:45 -05:00
Umbreon
e136529ff0 Add a check to getProperty for unknown properties. (#493) 2021-10-28 01:09:20 -04:00
106 changed files with 3900 additions and 1934 deletions

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

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

View File

@@ -11,29 +11,49 @@ on:
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
strategy:
matrix:
rust_version: [stable, "1.46.0"]
rust_version: [stable, 1.55.0]
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
with:
submodules: true
- name: Setup Rust toolchain
run: rustup default ${{ matrix.rust_version }}
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust_version }}
override: true
profile: minimal
- name: Build
run: cargo build --locked --verbose
- name: Run tests
- name: Test
run: cargo test --locked --verbose
- name: Rustfmt and Clippy
run: |
cargo fmt -- --check
cargo clippy
if: matrix.rust_version == 'stable'
lint:
name: Rustfmt and Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Rustfmt
run: cargo fmt -- --check
- name: Clippy
run: cargo clippy

View File

@@ -2,65 +2,152 @@ name: Release
on:
push:
tags: ["*"]
tags: ["v*"]
jobs:
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Build release binary
run: cargo build --verbose --locked --release
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: rojo-win64
path: target/release/rojo.exe
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Install Rust
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- name: Build release binary
run: |
source $HOME/.cargo/env
cargo build --verbose --locked --release
env:
OPENSSL_STATIC: 1
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: rojo-macos
path: target/release/rojo
linux:
create-release:
name: Create Release
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: false
- name: Build
run: cargo build --locked --verbose --release
env:
OPENSSL_STATIC: 1
build-plugin:
needs: ["create-release"]
name: Build Roblox Studio Plugin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: rojo-linux
path: target/release/rojo
- name: Setup Foreman
uses: Roblox/setup-foreman@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Plugin
run: rojo build plugin --output Rojo.rbxm
- name: Upload Plugin 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: Rojo.rbxm
asset_name: Rojo.rbxm
asset_content_type: application/octet-stream
- name: Upload Plugin to Artifacts
uses: actions/upload-artifact@v3
with:
name: Rojo.rbxm
path: Rojo.rbxm
build:
needs: ["create-release"]
strategy:
fail-fast: false
matrix:
# https://doc.rust-lang.org/rustc/platform-support.html
#
# FIXME: After the Rojo VS Code extension updates, add architecture
# names to each of these releases. We'll rename win64 to windows and add
# -x86_64 to each release.
include:
- host: linux
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
label: linux
- host: windows
os: windows-latest
target: x86_64-pc-windows-msvc
label: win64
- host: macos
os: macos-latest
target: x86_64-apple-darwin
label: macos
- host: macos
os: macos-latest
target: aarch64-apple-darwin
label: macos-aarch64
name: Build (${{ matrix.target }})
runs-on: ${{ matrix.os }}
env:
BIN: rojo
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Get Version from Tag
shell: bash
# https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
run: |
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
echo "Version is: ${{ env.PROJECT_VERSION }}"
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
override: true
profile: minimal
- name: Build Release
run: cargo build --release --locked --verbose
env:
# Build into a known directory so we can find our build artifact more
# easily.
CARGO_TARGET_DIR: output
# On platforms that use OpenSSL, ensure it is statically linked to
# make binaries more portable.
OPENSSL_STATIC: 1
- name: Create Release Archive
shell: bash
run: |
mkdir staging
if [ "${{ matrix.host }}" = "windows" ]; then
cp "output/release/$BIN.exe" staging/
cd staging
7z a ../release.zip *
else
cp "output/release/$BIN" staging/
cd staging
zip ../release.zip *
fi
- name: Upload Archive to Release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: release.zip
asset_name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
asset_content_type: application/octet-stream
- name: Upload Archive to Artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
path: release.zip

View File

@@ -1,6 +1,48 @@
# Rojo Changelog
## Unreleased Changes
* Switched from structopt to clap for command line argument parsing.
* Significantly improved performance of building and serving. ([#548])
* Fixed `init.meta.json` when used with `init.lua` and related files. ([#549])
[#548]: https://github.com/rojo-rbx/rojo/pull/548
[#549]: https://github.com/rojo-rbx/rojo/pull/549
## [7.1.1] - May 26, 2022
* Fixed sourcemap command not stripping paths correctly ([#544])
* Fixed Studio plugin settings not saving correctly.
[#544]: https://github.com/rojo-rbx/rojo/pull/544
[#545]: https://github.com/rojo-rbx/rojo/pull/545
[7.1.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.1.1
## [7.1.0] - May 22, 2022
* Added support for specifying an address to be used by default in project files. ([#507])
* Added support for optional paths in project files. ([#472])
* Added support for the new Open Cloud API when uploading. ([#504])
* Added `sourcemap` command for generating sourcemaps to feed into other tools. ([#530])
* Added PluginActions for connecting/disconnecting a session ([#537])
* Added changing toolbar icon to indicate state ([#538])
[#472]: https://github.com/rojo-rbx/rojo/pull/472
[#504]: https://github.com/rojo-rbx/rojo/pull/504
[#507]: https://github.com/rojo-rbx/rojo/pull/507
[#530]: https://github.com/rojo-rbx/rojo/pull/530
[#537]: https://github.com/rojo-rbx/rojo/pull/537
[#538]: https://github.com/rojo-rbx/rojo/pull/538
[7.1.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.1.0
## [7.0.0] - December 10, 2021
* Fixed Rojo's interactions with properties enabled by FFlags that are not yet enabled. ([#493])
* Improved output in Roblox Studio plugin when bad property data is encountered.
* Reintroduced support for CFrame shorthand syntax in Rojo project and `.meta.json` files, matching Rojo 6. ([#430])
* Connection settings are now remembered when reconnecting in Roblox Studio. ([#500])
* Updated reflection database to Roblox v503.
[#430]: https://github.com/rojo-rbx/rojo/issues/430
[#493]: https://github.com/rojo-rbx/rojo/pull/493
[#500]: https://github.com/rojo-rbx/rojo/pull/500
[7.0.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0
## [7.0.0-rc.3] - October 19, 2021
This is the last release candidate for Rojo 7. In an effort to get Rojo 7 out the door, we'll be freezing features from here on out, something we should've done a couple months ago.
@@ -75,7 +117,7 @@ The shorthand property format that most users use is not impacted. For reference
## [7.0.0-alpha.4][7.0.0-alpha.4] (May 5, 2021)
* Added the `gameId` and `placeId` optional properties to project files.
* When connecting from the Rojo Roblox Studio plugin, Rojo will set the game and place ID of the current place to these values, if set.
* This is equivalent to running `game:SetUniverseId(...)` and `game:SetPlaceId(...)` from the command bar in Studio.
* This is equivalent to running `game:SetUniverseId(...)` and `game:SetPlaceId(...)` from the command bar in Studio.
* Added "EXPERIMENTAL!" label to two-way sync toggle in Rojo's Roblox Studio plugin.
* Fixed `Name` and `Parent` properties being allowed in Rojo projects. ([#413][pr-413])
* Fixed "Open Scripts Externally" feature crashing Studio. ([#369][issue-369])

View File

@@ -29,6 +29,11 @@ Sometimes there's something that Rojo doesn't do that it probably should.
Please file issues and we'll try to help figure out what the best way forward is.
## Local Development Gotchas
If your build fails with "Error: failed to open file `D:\code\rojo\plugin\modules\roact\src`" you need to update your Git submodules.
Run the command and try building again: `git submodule update --init --recursive`.
## Pushing a Rojo Release
The Rojo release process is pretty manual right now. If you need to do it, here's how:
@@ -44,11 +49,9 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
* `cargo publish`
8. Publish the Plugin
* `cargo run -- upload plugin --asset_id 6415005344`
* `cargo run -- build plugin --output Rojo.rbxm`
9. Push commits and tags
* `git push && git push --tags`
10. Copy GitHub release content from previous release
* Update the leading text with a summary about the release
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
* Write a small summary of each major feature
* Attach release artifacts from GitHub Actions for each platform
* Write a small summary of each major feature

1912
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "rojo"
version = "7.0.0-rc.3"
version = "7.1.1"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0"
@@ -27,11 +27,10 @@ default = []
# Enable this feature to live-reload assets from the web UI.
dev_live_assets = []
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
[workspace]
members = [
"rojo-insta-ext",
"memofs",
]
members = ["crates/*"]
[lib]
name = "librojo"
@@ -42,7 +41,7 @@ name = "build"
harness = false
[dependencies]
memofs = { version = "0.2.0", path = "memofs" }
memofs = { version = "0.2.0", path = "crates/memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
@@ -52,7 +51,7 @@ memofs = { version = "0.2.0", path = "memofs" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.6.4"
rbx_dom_weak = "2.3.0"
rbx_dom_weak = "2.4.0"
rbx_reflection = "4.2.0"
rbx_reflection_database = "0.2.2"
rbx_xml = "0.12.3"
@@ -69,29 +68,28 @@ globset = "0.4.8"
humantime = "2.1.0"
hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
jod-thread = "0.1.2"
lazy_static = "1.4.0"
log = "0.4.14"
maplit = "1.0.2"
notify = "4.0.17"
opener = "0.5.0"
regex = "1.5.4"
reqwest = "0.9.24"
reqwest = { version = "0.11.10", features = ["blocking", "json"] }
ritz = "0.1.0"
rlua = "0.17.1"
roblox_install = "1.0.0"
serde = { version = "1.0.130", features = ["derive", "rc"] }
serde_json = "1.0.68"
structopt = "0.3.23"
termcolor = "1.1.2"
thiserror = "1.0.30"
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
uuid = { version = "0.8.2", features = ["v4", "serde"] }
uuid = { version = "1.0.0", features = ["v4", "serde"] }
clap = { version = "3.1.18", features = ["derive"] }
profiling = "1.0.6"
tracy-client = { version = "0.13.2", optional = true }
[target.'cfg(windows)'.dependencies]
winreg = "0.9.0"
winreg = "0.10.1"
[build-dependencies]
memofs = { version = "0.2.0", path = "memofs" }
memofs = { version = "0.2.0", path = "crates/memofs" }
embed-resource = "1.6.4"
anyhow = "1.0.44"
@@ -100,13 +98,12 @@ fs-err = "2.6.0"
maplit = "1.0.2"
[dev-dependencies]
rojo-insta-ext = { path = "rojo-insta-ext" }
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
criterion = "0.3.5"
insta = { version = "1.8.0", features = ["redactions"] }
lazy_static = "1.4.0"
paste = "1.0.5"
pretty_assertions = "0.7.2"
pretty_assertions = "1.2.1"
serde_yaml = "0.8.21"
tempfile = "3.2.0"
walkdir = "2.3.2"

BIN
assets/icon-link-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/icon-warn-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
[tools]
rojo = { source = "rojo-rbx/rojo", version = "6.1.0" }
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" }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Dictionary = require(Plugin.Dictionary)
local StudioPluginContext = require(script.Parent.StudioPluginContext)
local e = Roact.createElement
local StudioPluginAction = Roact.Component:extend("StudioPluginAction")
function StudioPluginAction:init()
self.pluginAction = self.props.plugin:CreatePluginAction(
self.props.name, self.props.title, self.props.description, self.props.icon, self.props.bindable
)
self.pluginAction.Triggered:Connect(self.props.onTriggered)
end
function StudioPluginAction:render()
return nil
end
function StudioPluginAction:willUnmount()
self.pluginAction:Destroy()
end
local function StudioPluginActionWrapper(props)
return e(StudioPluginContext.Consumer, {
render = function(plugin)
return e(StudioPluginAction, Dictionary.merge(props, {
plugin = plugin,
}))
end,
})
end
return StudioPluginActionWrapper

View File

@@ -44,6 +44,10 @@ function StudioToggleButton:didUpdate(lastProps)
self.button.Enabled = self.props.enabled
end
if self.props.icon ~= lastProps.icon then
self.button.Icon = self.props.icon
end
if self.props.active ~= lastProps.active then
self.button:SetActive(self.props.active)
end
@@ -63,4 +67,4 @@ local function StudioToggleButtonWrapper(props)
})
end
return StudioToggleButtonWrapper
return StudioToggleButtonWrapper

View File

@@ -24,7 +24,7 @@ local function AddressEntry(props)
layoutOrder = props.layoutOrder,
}, {
Host = e("TextBox", {
Text = "",
Text = props.host or "",
Font = Enum.Font.Code,
TextSize = 18,
TextColor3 = theme.AddressEntry.TextColor,
@@ -32,6 +32,7 @@ local function AddressEntry(props)
TextTransparency = props.transparency,
PlaceholderText = Config.defaultHost,
PlaceholderColor3 = theme.AddressEntry.PlaceholderColor,
ClearTextOnFocus = false,
Size = UDim2.new(1, -(HOST_OFFSET + DIVIDER_WIDTH + PORT_WIDTH), 1, 0),
Position = UDim2.new(0, HOST_OFFSET, 0, 0),
@@ -39,17 +40,22 @@ local function AddressEntry(props)
ClipsDescendants = true,
BackgroundTransparency = 1,
[Roact.Ref] = props.hostRef,
[Roact.Change.Text] = function(object)
if props.onHostChange ~= nil then
props.onHostChange(object.Text)
end
end
}),
Port = e("TextBox", {
Text = "",
Text = props.port or "",
Font = Enum.Font.Code,
TextSize = 18,
TextColor3 = theme.AddressEntry.TextColor,
TextTransparency = props.transparency,
PlaceholderText = Config.defaultPort,
PlaceholderColor3 = theme.AddressEntry.PlaceholderColor,
ClearTextOnFocus = false,
Size = UDim2.new(0, PORT_WIDTH, 1, 0),
Position = UDim2.new(1, 0, 0, 0),
@@ -58,12 +64,14 @@ local function AddressEntry(props)
ClipsDescendants = true,
BackgroundTransparency = 1,
[Roact.Ref] = props.portRef,
[Roact.Change.Text] = function(object)
local text = object.Text
text = text:gsub("%D", "")
object.Text = text
if props.onPortChange ~= nil then
props.onPortChange(text)
end
end,
}, {
Divider = e("Frame", {
@@ -80,11 +88,6 @@ end
local NotConnectedPage = Roact.Component:extend("NotConnectedPage")
function NotConnectedPage:init()
self.hostRef = Roact.createRef()
self.portRef = Roact.createRef()
end
function NotConnectedPage:render()
return Roact.createFragment({
Header = e(Header, {
@@ -93,8 +96,10 @@ function NotConnectedPage:render()
}),
AddressEntry = e(AddressEntry, {
hostRef = self.hostRef,
portRef = self.portRef,
host = self.props.host,
port = self.props.port,
onHostChange = self.props.onHostChange,
onPortChange = self.props.onPortChange,
transparency = self.props.transparency,
layoutOrder = 2,
}),
@@ -117,15 +122,7 @@ function NotConnectedPage:render()
style = "Solid",
transparency = self.props.transparency,
layoutOrder = 2,
onClick = function()
local hostText = self.hostRef.current.Text
local portText = self.portRef.current.Text
self.props.onConnect(
#hostText > 0 and hostText or Config.defaultHost,
#portText > 0 and portText or Config.defaultPort
)
end,
onClick = self.props.onConnect,
}),
Layout = e("UIListLayout", {

View File

@@ -16,6 +16,7 @@ local Theme = require(script.Theme)
local PluginSettings = require(script.PluginSettings)
local Page = require(script.Page)
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
local StudioToggleButton = require(script.Components.Studio.StudioToggleButton)
local StudioPluginGui = require(script.Components.Studio.StudioPluginGui)
@@ -37,13 +38,34 @@ local App = Roact.Component:extend("App")
function App:init()
preloadAssets()
self.host, self.setHost = Roact.createBinding("")
self.port, self.setPort = Roact.createBinding("")
self:setState({
appStatus = AppStatus.NotConnected,
guiEnabled = false,
toolbarIcon = Assets.Images.PluginButton,
})
end
function App:startSession(host, port, sessionOptions)
function App:getHostAndPort()
local host = self.host:getValue()
local port = self.port:getValue()
local host = if #host > 0 then host else Config.defaultHost
local port = if #port > 0 then port else Config.defaultPort
return host, port
end
function App:startSession()
local host, port = self:getHostAndPort()
local sessionOptions = {
openScriptsExternally = self.props.settings:get("openScriptsExternally"),
twoWaySync = self.props.settings:get("twoWaySync"),
}
local baseUrl = ("http://%s:%s"):format(host, port)
local apiContext = ApiContext.new(baseUrl)
@@ -57,6 +79,7 @@ function App:startSession(host, port, sessionOptions)
if status == ServeSession.Status.Connecting then
self:setState({
appStatus = AppStatus.Connecting,
toolbarIcon = Assets.Images.PluginButton,
})
elseif status == ServeSession.Status.Connected then
local address = ("%s:%s"):format(host, port)
@@ -64,7 +87,10 @@ function App:startSession(host, port, sessionOptions)
appStatus = AppStatus.Connected,
projectName = details,
address = address,
toolbarIcon = Assets.Images.PluginButtonConnected,
})
Log.info("Connected to session '{}' at {}", details, address)
elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil
@@ -76,11 +102,15 @@ function App:startSession(host, port, sessionOptions)
self:setState({
appStatus = AppStatus.Error,
errorMessage = tostring(details),
toolbarIcon = Assets.Images.PluginButtonWarning,
})
else
self:setState({
appStatus = AppStatus.NotConnected,
toolbarIcon = Assets.Images.PluginButton,
})
Log.info("Disconnected session")
end
end
end)
@@ -90,6 +120,22 @@ function App:startSession(host, port, sessionOptions)
self.serveSession = serveSession
end
function App:endSession()
if self.serveSession == nil then
return
end
Log.trace("Disconnecting session")
self.serveSession:stop()
self.serveSession = nil
self:setState({
appStatus = AppStatus.NotConnected,
})
Log.trace("Session terminated by user")
end
function App:render()
local pluginName = "Rojo " .. Version.display(Config.version)
@@ -108,119 +154,160 @@ function App:render()
value = self.props.plugin,
}, {
e(Theme.StudioProvider, nil, {
e(PluginSettings.StudioProvider, {
plugin = self.props.plugin,
gui = e(StudioPluginGui, {
id = pluginName,
title = pluginName,
active = self.state.guiEnabled,
initDockState = Enum.InitialDockState.Right,
initEnabled = false,
overridePreviousState = false,
floatingSize = Vector2.new(300, 200),
minimumSize = Vector2.new(300, 200),
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
onInitialState = function(initialState)
self:setState({
guiEnabled = initialState,
})
end,
onClose = function()
self:setState({
guiEnabled = false,
})
end,
}, {
gui = e(StudioPluginGui, {
id = pluginName,
title = pluginName,
active = self.state.guiEnabled,
NotConnectedPage = createPageElement(AppStatus.NotConnected, {
host = self.host,
onHostChange = self.setHost,
port = self.port,
onPortChange = self.setPort,
initDockState = Enum.InitialDockState.Right,
initEnabled = false,
overridePreviousState = false,
floatingSize = Vector2.new(300, 200),
minimumSize = Vector2.new(300, 200),
onConnect = function()
self:startSession()
end,
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
onInitialState = function(initialState)
onNavigateSettings = function()
self:setState({
guiEnabled = initialState,
appStatus = AppStatus.Settings,
})
end,
}),
Connecting = createPageElement(AppStatus.Connecting),
Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName,
address = self.state.address,
onDisconnect = function()
self:endSession()
end,
}),
Settings = createPageElement(AppStatus.Settings, {
onBack = function()
self:setState({
appStatus = AppStatus.NotConnected,
})
end,
}),
Error = createPageElement(AppStatus.Error, {
errorMessage = self.state.errorMessage,
onClose = function()
self:setState({
guiEnabled = false,
appStatus = AppStatus.NotConnected,
toolbarIcon = Assets.Images.PluginButton,
})
end,
}, {
NotConnectedPage = PluginSettings.with(function(settings)
return createPageElement(AppStatus.NotConnected, {
onConnect = function(host, port)
self:startSession(host, port, {
openScriptsExternally = settings:get("openScriptsExternally"),
twoWaySync = settings:get("twoWaySync"),
})
end,
onNavigateSettings = function()
self:setState({
appStatus = AppStatus.Settings,
})
end,
})
end),
Connecting = createPageElement(AppStatus.Connecting),
Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName,
address = self.state.address,
onDisconnect = function()
Log.trace("Disconnecting session")
self.serveSession:stop()
self.serveSession = nil
self:setState({
appStatus = AppStatus.NotConnected,
})
Log.trace("Session terminated by user")
end,
}),
Settings = createPageElement(AppStatus.Settings, {
onBack = function()
self:setState({
appStatus = AppStatus.NotConnected,
})
end,
}),
Error = createPageElement(AppStatus.Error, {
errorMessage = self.state.errorMessage,
onClose = function()
self:setState({
appStatus = AppStatus.NotConnected,
})
end,
}),
Background = Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = theme.BackgroundColor,
ZIndex = 0,
BorderSizePixel = 0,
})
end),
}),
toolbar = e(StudioToolbar, {
name = pluginName,
}, {
button = e(StudioToggleButton, {
name = "Rojo",
tooltip = "Show or hide the Rojo panel",
icon = Assets.Images.PluginButton,
active = self.state.guiEnabled,
enabled = true,
onClick = function()
self:setState(function(state)
return {
guiEnabled = not state.guiEnabled,
}
end)
end,
Background = Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = theme.BackgroundColor,
ZIndex = 0,
BorderSizePixel = 0,
})
}),
end),
}),
toggleAction = e(StudioPluginAction, {
name = "RojoConnection",
title = "Rojo: Connect/Disconnect",
description = "Toggles the server for a Rojo sync session",
icon = Assets.Images.PluginButton,
bindable = true,
onTriggered = function()
if self.serveSession == nil or self.serveSession:getStatus() == ServeSession.Status.NotStarted then
self:startSession()
elseif self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected then
self:endSession()
end
end,
}),
connectAction = e(StudioPluginAction, {
name = "RojoConnect",
title = "Rojo: Connect",
description = "Connects the server for a Rojo sync session",
icon = Assets.Images.PluginButton,
bindable = true,
onTriggered = function()
if self.serveSession == nil or self.serveSession:getStatus() == ServeSession.Status.NotStarted then
self:startSession()
end
end,
}),
disconnectAction = e(StudioPluginAction, {
name = "RojoDisconnect",
title = "Rojo: Disconnect",
description = "Disconnects the server for a Rojo sync session",
icon = Assets.Images.PluginButton,
bindable = true,
onTriggered = function()
if self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected then
self:endSession()
end
end,
}),
toolbar = e(StudioToolbar, {
name = pluginName,
}, {
button = e(StudioToggleButton, {
name = "Rojo",
tooltip = "Show or hide the Rojo panel",
icon = self.state.toolbarIcon,
active = self.state.guiEnabled,
enabled = true,
onClick = function()
self:setState(function(state)
return {
guiEnabled = not state.guiEnabled,
}
end)
end,
})
}),
}),
})
end
return App
return function(props)
return e(PluginSettings.StudioProvider, {
plugin = props.plugin,
}, {
App = PluginSettings.with(function(settings)
local settingsProps = Dictionary.merge(props, {
settings = settings,
})
return e(App, settingsProps)
end),
})
end

View File

@@ -18,6 +18,8 @@ local Assets = {
Images = {
Logo = "rbxassetid://5990772764",
PluginButton = "rbxassetid://3405341609",
PluginButtonConnected = "rbxassetid://9529783993",
PluginButtonWarning = "rbxassetid://9529784530",
Icons = {
Close = "rbxassetid://6012985953",
Back = "rbxassetid://6017213752",

View File

@@ -5,7 +5,7 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
return strict("Config", {
isDevBuild = isDevBuild,
codename = "Epiphany",
version = {7, 0, 0, "-rc.3"},
version = {7, 1, 1},
expectedServerVersionString = "7.0 or newer",
protocolVersion = 4,
defaultHost = "localhost",

View File

@@ -63,7 +63,7 @@ local function applyPatch(instanceMap, patch)
local failedToReify = reify(instanceMap, patch.added, id, parentInstance)
if not PatchSet.isEmpty(failedToReify) then
Log.debug("Failed to reify as part of applying a patch: {}", failedToReify)
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
PatchSet.assign(unappliedPatch, failedToReify)
end
end

View File

@@ -75,9 +75,13 @@ local function diff(instanceMap, virtualInstances, rootId)
changedProperties[propertyName] = virtualValue
end
else
-- virtualValue can be empty in certain cases, and this may print out nil to the user.
local propertyType = next(virtualValue)
Log.warn("Failed to decode property of type {}", propertyType)
Log.warn(
"Failed to decode property {}.{}. Encoded property was: {:#?}",
virtualInstance.ClassName,
propertyName,
virtualValue
)
end
else
local err = existingValueOrErr

View File

@@ -40,6 +40,13 @@ local function getProperty(instance, propertyName)
})
end
if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("is not a valid member of") then
return false, Error.new(Error.UnknownProperty, {
className = instance.ClassName,
propertyName = propertyName,
})
end
return false, Error.new(Error.OtherPropertyError, {
className = instance.ClassName,
propertyName = propertyName,

View File

@@ -113,6 +113,10 @@ function ServeSession:__fmtDebug(output)
output:write("}")
end
function ServeSession:getStatus()
return self.__status
end
function ServeSession:onStatusChanged(callback)
self.__statusChangedCallback = callback
end

View File

@@ -0,0 +1,14 @@
---
source: tests/tests/build.rs
assertion_line: 98
expression: contents
---
<roblox version="4">
<Item class="LocalScript" referent="0">
<Properties>
<string name="Name">issue_546</string>
<bool name="Disabled">true</bool>
<string name="Source">print("Hello, world!")</string>
</Properties>
</Item>
</roblox>

View File

@@ -0,0 +1,24 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">optional</string>
</Properties>
<Item class="StringValue" referent="1">
<Properties>
<string name="Name">foo-optional</string>
<string name="Value">Hello, from foo.txt!</string>
</Properties>
</Item>
<Item class="StringValue" referent="2">
<Properties>
<string name="Name">foo-required</string>
<string name="Value">Hello, from foo.txt!</string>
</Properties>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,17 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">root</string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">folder</string>
</Properties>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,22 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">root</string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">folder</string>
</Properties>
<Item class="Folder" referent="2">
<Properties>
<string name="Name">child-projectname</string>
</Properties>
</Item>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,12 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">root</string>
</Properties>
</Item>
</roblox>

View File

@@ -32,6 +32,20 @@ expression: contents
<Item class="Part" referent="4">
<Properties>
<string name="Name">Color</string>
<CoordinateFrame name="CFrame">
<X>1</X>
<Y>2</Y>
<Z>3</Z>
<R00>0</R00>
<R01>1</R01>
<R02>0</R02>
<R10>0</R10>
<R11>0</R11>
<R12>1</R12>
<R20>1</R20>
<R21>0</R21>
<R22>0</R22>
</CoordinateFrame>
<Color3uint8 name="Color3uint8">8404992</Color3uint8>
</Properties>
</Item>

View File

@@ -0,0 +1,230 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">weldconstraint</string>
<BinaryString name="AttributesSerialize">
</BinaryString>
<int64 name="SourceAssetId">-1</int64>
<BinaryString name="Tags"></BinaryString>
</Properties>
<Item class="Part" referent="1">
<Properties>
<string name="Name">A</string>
<bool name="Anchored">false</bool>
<BinaryString name="AttributesSerialize">
</BinaryString>
<float name="BackParamA">-0.5</float>
<float name="BackParamB">0.5</float>
<token name="BackSurface">0</token>
<token name="BackSurfaceInput">0</token>
<float name="BottomParamA">-0.5</float>
<float name="BottomParamB">0.5</float>
<token name="BottomSurface">0</token>
<token name="BottomSurfaceInput">0</token>
<CoordinateFrame name="CFrame">
<X>-14</X>
<Y>0.5</Y>
<Z>-5</Z>
<R00>1</R00>
<R01>0</R01>
<R02>0</R02>
<R10>0</R10>
<R11>1</R11>
<R12>0</R12>
<R20>0</R20>
<R21>0</R21>
<R22>1</R22>
</CoordinateFrame>
<bool name="CanCollide">true</bool>
<bool name="CanQuery">true</bool>
<bool name="CanTouch">true</bool>
<bool name="CastShadow">true</bool>
<int name="CollisionGroupId">0</int>
<Color3uint8 name="Color3uint8">10724005</Color3uint8>
<PhysicalProperties name="CustomPhysicalProperties">
<CustomPhysics>false</CustomPhysics>
</PhysicalProperties>
<token name="formFactorRaw">1</token>
<float name="FrontParamA">-0.5</float>
<float name="FrontParamB">0.5</float>
<token name="FrontSurface">0</token>
<token name="FrontSurfaceInput">0</token>
<float name="LeftParamA">-0.5</float>
<float name="LeftParamB">0.5</float>
<token name="LeftSurface">0</token>
<token name="LeftSurfaceInput">0</token>
<bool name="Locked">false</bool>
<bool name="Massless">false</bool>
<token name="Material">256</token>
<CoordinateFrame name="PivotOffset">
<X>0</X>
<Y>0</Y>
<Z>0</Z>
<R00>1</R00>
<R01>0</R01>
<R02>0</R02>
<R10>0</R10>
<R11>1</R11>
<R12>0</R12>
<R20>0</R20>
<R21>0</R21>
<R22>1</R22>
</CoordinateFrame>
<float name="Reflectance">0</float>
<float name="RightParamA">-0.5</float>
<float name="RightParamB">0.5</float>
<token name="RightSurface">0</token>
<token name="RightSurfaceInput">0</token>
<int name="RootPriority">0</int>
<Vector3 name="RotVelocity">
<X>0</X>
<Y>0</Y>
<Z>0</Z>
</Vector3>
<token name="shape">1</token>
<Vector3 name="size">
<X>4</X>
<Y>1</Y>
<Z>2</Z>
</Vector3>
<int64 name="SourceAssetId">-1</int64>
<BinaryString name="Tags"></BinaryString>
<float name="TopParamA">-0.5</float>
<float name="TopParamB">0.5</float>
<token name="TopSurface">0</token>
<token name="TopSurfaceInput">0</token>
<float name="Transparency">0</float>
<Vector3 name="Velocity">
<X>0</X>
<Y>0</Y>
<Z>0</Z>
</Vector3>
</Properties>
<Item class="WeldConstraint" referent="2">
<Properties>
<string name="Name">WeldConstraint</string>
<BinaryString name="AttributesSerialize">
</BinaryString>
<CoordinateFrame name="CFrame0">
<X>7</X>
<Y>0.000001013279</Y>
<Z>-3</Z>
<R00>1</R00>
<R01>0</R01>
<R02>0</R02>
<R10>0</R10>
<R11>1</R11>
<R12>0</R12>
<R20>0</R20>
<R21>0</R21>
<R22>1</R22>
</CoordinateFrame>
<Ref name="Part0Internal">1</Ref>
<Ref name="Part1Internal">3</Ref>
<int64 name="SourceAssetId">-1</int64>
<int name="State">3</int>
<BinaryString name="Tags"></BinaryString>
</Properties>
</Item>
</Item>
<Item class="Part" referent="3">
<Properties>
<string name="Name">B</string>
<bool name="Anchored">false</bool>
<BinaryString name="AttributesSerialize">
</BinaryString>
<float name="BackParamA">-0.5</float>
<float name="BackParamB">0.5</float>
<token name="BackSurface">0</token>
<token name="BackSurfaceInput">0</token>
<float name="BottomParamA">-0.5</float>
<float name="BottomParamB">0.5</float>
<token name="BottomSurface">0</token>
<token name="BottomSurfaceInput">0</token>
<CoordinateFrame name="CFrame">
<X>-7</X>
<Y>0.500001</Y>
<Z>-8</Z>
<R00>1</R00>
<R01>0</R01>
<R02>0</R02>
<R10>0</R10>
<R11>1</R11>
<R12>0</R12>
<R20>0</R20>
<R21>0</R21>
<R22>1</R22>
</CoordinateFrame>
<bool name="CanCollide">true</bool>
<bool name="CanQuery">true</bool>
<bool name="CanTouch">true</bool>
<bool name="CastShadow">true</bool>
<int name="CollisionGroupId">0</int>
<Color3uint8 name="Color3uint8">10724005</Color3uint8>
<PhysicalProperties name="CustomPhysicalProperties">
<CustomPhysics>false</CustomPhysics>
</PhysicalProperties>
<token name="formFactorRaw">1</token>
<float name="FrontParamA">-0.5</float>
<float name="FrontParamB">0.5</float>
<token name="FrontSurface">0</token>
<token name="FrontSurfaceInput">0</token>
<float name="LeftParamA">-0.5</float>
<float name="LeftParamB">0.5</float>
<token name="LeftSurface">0</token>
<token name="LeftSurfaceInput">0</token>
<bool name="Locked">false</bool>
<bool name="Massless">false</bool>
<token name="Material">256</token>
<CoordinateFrame name="PivotOffset">
<X>0</X>
<Y>0</Y>
<Z>0</Z>
<R00>1</R00>
<R01>0</R01>
<R02>0</R02>
<R10>0</R10>
<R11>1</R11>
<R12>0</R12>
<R20>0</R20>
<R21>0</R21>
<R22>1</R22>
</CoordinateFrame>
<float name="Reflectance">0</float>
<float name="RightParamA">-0.5</float>
<float name="RightParamB">0.5</float>
<token name="RightSurface">0</token>
<token name="RightSurfaceInput">0</token>
<int name="RootPriority">0</int>
<Vector3 name="RotVelocity">
<X>0</X>
<Y>0</Y>
<Z>0</Z>
</Vector3>
<token name="shape">1</token>
<Vector3 name="size">
<X>4</X>
<Y>1</Y>
<Z>2</Z>
</Vector3>
<int64 name="SourceAssetId">-1</int64>
<BinaryString name="Tags"></BinaryString>
<float name="TopParamA">-0.5</float>
<float name="TopParamB">0.5</float>
<token name="TopSurface">0</token>
<token name="TopSurfaceInput">0</token>
<float name="Transparency">0</float>
<Vector3 name="Velocity">
<X>0</X>
<Y>0</Y>
<Z>0</Z>
</Vector3>
</Properties>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,2 @@
# Issue #546 (https://github.com/rojo-rbx/rojo/issues/546)
Regression from Rojo 6.2.0 to Rojo 7.0.0. Meta files named as init.meta.json should apply after init.client.lua and other init files.

View File

@@ -0,0 +1,6 @@
{
"name": "issue_546",
"tree": {
"$path": "hello"
}
}

View File

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

View File

@@ -0,0 +1,5 @@
{
"properties": {
"Disabled": true
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "optional",
"tree": {
"$className": "Folder",
"foo-required": {
"$path": "foo.txt"
},
"foo-optional":{
"$path": { "optional": "foo.txt" }
},
"bar-optional":{
"$path": { "optional": "bar.txt" }
}
}
}

View File

@@ -0,0 +1 @@
Hello, from foo.txt!

View File

@@ -0,0 +1,9 @@
{
"name": "root",
"tree": {
"$className": "Folder",
"folder": {
"$path": "folder"
}
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "child-projectname",
"tree": {
"$className": "Folder"
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "root",
"tree": {
"$className": "Folder",
"folder": {
"$path": "folder"
}
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "child-projectname",
"tree": {
"$className": "Folder"
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "root",
"tree": {
"$className": "Folder"
}
}

View File

@@ -14,7 +14,13 @@
"Color": {
"$className": "Part",
"$properties": {
"Color": [0.5, 0.25, 0]
"Color": [0.5, 0.25, 0],
"CFrame": [
1, 2, 3,
0, 1, 0,
0, 0, 1,
1, 0, 0
]
}
},

View File

@@ -0,0 +1,6 @@
{
"name": "weldconstraint",
"tree": {
"$path": "two-parts-welded.rbxmx"
}
}

View File

@@ -0,0 +1,14 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: optional
protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -0,0 +1,62 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children:
- id-3
ClassName: Folder
Id: id-2
Metadata:
ignoreUnknownInstances: true
Name: optional
Parent: "00000000000000000000000000000000"
Properties: {}
id-3:
Children:
- id-4
- id-5
ClassName: Folder
Id: id-3
Metadata:
ignoreUnknownInstances: false
Name: src
Parent: id-2
Properties: {}
id-4:
Children: []
ClassName: StringValue
Id: id-4
Metadata:
ignoreUnknownInstances: false
Name: foo
Parent: id-3
Properties:
Value:
String: "Hello, from foo.txt!"
id-5:
Children:
- id-6
ClassName: Folder
Id: id-5
Metadata:
ignoreUnknownInstances: false
Name: node_modules
Parent: id-3
Properties: {}
id-6:
Children: []
ClassName: StringValue
Id: id-6
Metadata:
ignoreUnknownInstances: false
Name: bar
Parent: id-5
Properties:
Value:
String: Hello from bar.txt
messageCursor: 2
sessionId: id-1

View File

@@ -0,0 +1,40 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children:
- id-3
ClassName: Folder
Id: id-2
Metadata:
ignoreUnknownInstances: true
Name: optional
Parent: "00000000000000000000000000000000"
Properties: {}
id-3:
Children:
- id-4
ClassName: Folder
Id: id-3
Metadata:
ignoreUnknownInstances: false
Name: src
Parent: id-2
Properties: {}
id-4:
Children: []
ClassName: StringValue
Id: id-4
Metadata:
ignoreUnknownInstances: false
Name: foo
Parent: id-3
Properties:
Value:
String: "Hello, from foo.txt!"
messageCursor: 0
sessionId: id-1

View File

@@ -0,0 +1,36 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 2
messages:
- added:
id-5:
Children:
- id-6
ClassName: Folder
Id: id-5
Metadata:
ignoreUnknownInstances: false
Name: node_modules
Parent: id-3
Properties: {}
id-6:
Children: []
ClassName: StringValue
Id: id-6
Metadata:
ignoreUnknownInstances: false
Name: bar
Parent: id-5
Properties:
Value:
String: Hello from bar.txt
removed: []
updated: []
- added: {}
removed: []
updated: []
sessionId: id-1

View File

@@ -0,0 +1,9 @@
{
"name": "optional",
"tree": {
"$className": "Folder",
"create-later": {
"$path": { "optional": "create-later" }
}
}
}

View File

@@ -1,4 +1,4 @@
std = "roblox"
std = "roblox+testez"
[config]
unused_variable = { allow_unused_self = true }

View File

@@ -284,22 +284,14 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
// that path and use it as the source for our patch.
let snapshot = match snapshot_from_vfs(&metadata.context, &vfs, &path) {
Ok(Some(snapshot)) => snapshot,
Ok(None) => {
log::error!(
"Snapshot did not return an instance from path {}",
path.display()
);
log::error!("This may be a bug!");
return None;
}
Ok(snapshot) => snapshot,
Err(err) => {
log::error!("Snapshot error: {:?}", err);
return None;
}
};
let patch_set = compute_patch_set(&snapshot, &tree, id);
let patch_set = compute_patch_set(snapshot, &tree, id);
apply_patch_set(tree, patch_set)
}
Ok(None) => {
@@ -335,19 +327,14 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
);
let snapshot = match snapshot_result {
Ok(Some(snapshot)) => snapshot,
Ok(None) => {
log::error!("Snapshot did not return an instance from a project node.");
log::error!("This is a bug!");
return None;
}
Ok(snapshot) => snapshot,
Err(err) => {
log::error!("{:?}", err);
return None;
}
};
let patch_set = compute_patch_set(&snapshot, &tree, id);
let patch_set = compute_patch_set(snapshot, &tree, id);
apply_patch_set(tree, patch_set)
}
};

View File

@@ -1,12 +1,13 @@
use std::{
io::{BufWriter, Write},
mem::forget,
path::{Path, PathBuf},
};
use anyhow::Context;
use clap::Parser;
use fs_err::File;
use memofs::Vfs;
use structopt::StructOpt;
use tokio::runtime::Runtime;
use crate::serve_session::ServeSession;
@@ -17,20 +18,20 @@ const UNKNOWN_OUTPUT_KIND_ERR: &str = "Could not detect what kind of file to bui
Expected output file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx.";
/// Generates a model or place file from the Rojo project.
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub struct BuildCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = "")]
#[clap(default_value = "")]
pub project: PathBuf,
/// Where to output the result.
///
/// Should end in .rbxm, .rbxl, .rbxmx, or .rbxlx.
#[structopt(long, short)]
#[clap(long, short)]
pub output: PathBuf,
/// Whether to automatically rebuild when any input files change.
#[structopt(long)]
#[clap(long)]
pub watch: bool,
}
@@ -61,6 +62,10 @@ impl BuildCommand {
}
}
// Avoid dropping ServeSession: it's potentially VERY expensive to drop
// and we're about to exit anyways.
forget(session);
Ok(())
}
}
@@ -97,6 +102,7 @@ fn xml_encode_config() -> rbx_xml::EncodeOptions {
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
}
#[profiling::function]
fn write_model(
session: &ServeSession,
output: &Path,

View File

@@ -1,7 +1,7 @@
use structopt::StructOpt;
use clap::Parser;
/// Open Rojo's documentation in your browser.
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub struct DocCommand {}
impl DocCommand {

View File

@@ -1,15 +1,15 @@
use std::path::PathBuf;
use anyhow::Context;
use structopt::StructOpt;
use clap::Parser;
use crate::project::Project;
/// Reformat a Rojo project using the standard JSON formatting rules.
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub struct FmtProjectCommand {
/// Path to the project to format. Defaults to the current directory.
#[structopt(default_value = "")]
#[clap(default_value = "")]
pub project: PathBuf,
}

View File

@@ -4,9 +4,9 @@ use std::process::{Command, Stdio};
use std::str::FromStr;
use anyhow::{bail, format_err};
use clap::Parser;
use fs_err as fs;
use fs_err::OpenOptions;
use structopt::StructOpt;
use super::resolve_path;
@@ -22,14 +22,14 @@ static PLACE_README: &str = include_str!("../../assets/default-place-project/REA
static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
/// Initializes a new Rojo project.
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub struct InitCommand {
/// Path to the place to create the project. Defaults to the current directory.
#[structopt(default_value = "")]
#[clap(default_value = "")]
pub path: PathBuf,
/// The kind of project to create, 'place' or 'model'. Defaults to place.
#[structopt(long, default_value = "place")]
#[clap(long, default_value = "place")]
pub kind: InitKind,
}

View File

@@ -1,4 +1,4 @@
//! Defines Rojo's CLI through structopt types.
//! Defines Rojo's CLI through clap types.
mod build;
mod doc;
@@ -6,11 +6,13 @@ mod fmt_project;
mod init;
mod plugin;
mod serve;
mod sourcemap;
mod unpack;
mod upload;
use std::{borrow::Cow, env, path::Path, str::FromStr};
use structopt::StructOpt;
use clap::Parser;
use thiserror::Error;
pub use self::build::BuildCommand;
@@ -19,17 +21,19 @@ pub use self::fmt_project::FmtProjectCommand;
pub use self::init::{InitCommand, InitKind};
pub use self::plugin::{PluginCommand, PluginSubcommand};
pub use self::serve::ServeCommand;
pub use self::sourcemap::SourcemapCommand;
pub use self::unpack::UnpackCommand;
pub use self::upload::UploadCommand;
/// Command line options that Rojo accepts, defined using the structopt crate.
#[derive(Debug, StructOpt)]
#[structopt(name = "Rojo", about, author)]
/// Command line options that Rojo accepts, defined using the clap crate.
#[derive(Debug, Parser)]
#[clap(name = "Rojo", version, about, author)]
pub struct Options {
#[structopt(flatten)]
#[clap(flatten)]
pub global: GlobalOptions,
/// Subcommand to run in this invocation.
#[structopt(subcommand)]
#[clap(subcommand)]
pub subcommand: Subcommand,
}
@@ -40,21 +44,23 @@ impl Options {
Subcommand::Serve(subcommand) => subcommand.run(self.global),
Subcommand::Build(subcommand) => subcommand.run(),
Subcommand::Upload(subcommand) => subcommand.run(),
Subcommand::Sourcemap(subcommand) => subcommand.run(),
Subcommand::FmtProject(subcommand) => subcommand.run(),
Subcommand::Doc(subcommand) => subcommand.run(),
Subcommand::Plugin(subcommand) => subcommand.run(),
Subcommand::Unpack(subcommand) => subcommand.run(),
}
}
}
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub struct GlobalOptions {
/// Sets verbosity level. Can be specified multiple times.
#[structopt(long("verbose"), short, global(true), parse(from_occurrences))]
#[clap(long("verbose"), short, global(true), parse(from_occurrences))]
pub verbosity: u8,
/// Set color behavior. Valid values are auto, always, and never.
#[structopt(long("color"), global(true), default_value("auto"))]
#[clap(long("color"), global(true), default_value("auto"))]
pub color: ColorChoice,
}
@@ -106,15 +112,17 @@ pub struct ColorChoiceParseError {
attempted: String,
}
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub enum Subcommand {
Init(InitCommand),
Serve(ServeCommand),
Build(BuildCommand),
Upload(UploadCommand),
Sourcemap(SourcemapCommand),
FmtProject(FmtProjectCommand),
Doc(DocCommand),
Plugin(PluginCommand),
Unpack(UnpackCommand),
}
pub(super) fn resolve_path(path: &Path) -> Cow<'_, Path> {

View File

@@ -3,9 +3,9 @@ use std::{
io::BufWriter,
};
use clap::Parser;
use memofs::{InMemoryFs, Vfs, VfsSnapshot};
use roblox_install::RobloxStudio;
use structopt::StructOpt;
use crate::serve_session::ServeSession;
@@ -13,14 +13,14 @@ static PLUGIN_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/plugin.
static PLUGIN_FILE_NAME: &str = "RojoManagedPlugin.rbxm";
/// Install Rojo's plugin.
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub struct PluginCommand {
#[structopt(subcommand)]
#[clap(subcommand)]
subcommand: PluginSubcommand,
}
/// Manages Rojo's Roblox Studio plugin.
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub enum PluginSubcommand {
/// Install the plugin in Roblox Studio's plugins folder. If the plugin is
/// already installed, installing it again will overwrite the current plugin

View File

@@ -5,8 +5,8 @@ use std::{
sync::Arc,
};
use clap::Parser;
use memofs::Vfs;
use structopt::StructOpt;
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
use crate::{serve_session::ServeSession, web::LiveServer};
@@ -17,19 +17,19 @@ const DEFAULT_BIND_ADDRESS: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const DEFAULT_PORT: u16 = 34872;
/// Expose a Rojo project to the Rojo Studio plugin.
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub struct ServeCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = "")]
#[clap(default_value = "")]
pub project: PathBuf,
/// The IP address to listen on. Defaults to `127.0.0.1`.
#[structopt(long)]
#[clap(long)]
pub address: Option<IpAddr>,
/// The port to listen on. Defaults to the project's preference, or `34872` if
/// it has none.
#[structopt(long)]
#[clap(long)]
pub port: Option<u16>,
}
@@ -41,7 +41,10 @@ impl ServeCommand {
let session = Arc::new(ServeSession::new(vfs, &project_path)?);
let ip = self.address.unwrap_or(DEFAULT_BIND_ADDRESS.into());
let ip = self
.address
.or_else(|| session.serve_address())
.unwrap_or(DEFAULT_BIND_ADDRESS.into());
let port = self
.port

136
src/cli/sourcemap.rs Normal file
View File

@@ -0,0 +1,136 @@
use std::{
io::{BufWriter, Write},
path::{Path, PathBuf},
};
use clap::Parser;
use fs_err::File;
use memofs::Vfs;
use rbx_dom_weak::types::Ref;
use serde::Serialize;
use crate::{
serve_session::ServeSession,
snapshot::{InstanceWithMeta, RojoTree},
};
use super::resolve_path;
const PATH_STRIP_FAILED_ERR: &str = "Failed to create relative paths for project file!";
/// Representation of a node in the generated sourcemap tree.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SourcemapNode {
name: String,
class_name: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
file_paths: Vec<PathBuf>,
#[serde(skip_serializing_if = "Vec::is_empty")]
children: Vec<SourcemapNode>,
}
/// Generates a sourcemap file from the Rojo project.
#[derive(Debug, Parser)]
pub struct SourcemapCommand {
/// Path to the project to use for the sourcemap. Defaults to the current
/// directory.
#[clap(default_value = "")]
pub project: PathBuf,
/// Where to output the sourcemap. Omit this to use stdout instead of
/// writing to a file.
///
/// Should end in .json.
#[clap(long, short)]
pub output: Option<PathBuf>,
/// If non-script files should be included or not. Defaults to false.
#[clap(long)]
pub include_non_scripts: bool,
}
impl SourcemapCommand {
pub fn run(self) -> anyhow::Result<()> {
let project_path = resolve_path(&self.project);
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new_default();
let session = ServeSession::new(vfs, &project_path)?;
let tree = session.tree();
let filter = if self.include_non_scripts {
filter_nothing
} else {
filter_non_scripts
};
let root_node = recurse_create_node(&tree, tree.get_root_id(), session.root_dir(), filter);
if let Some(output_path) = self.output {
let mut file = BufWriter::new(File::create(&output_path)?);
serde_json::to_writer(&mut file, &root_node)?;
file.flush()?;
println!("Created sourcemap at {}", output_path.display());
} else {
let output = serde_json::to_string(&root_node)?;
println!("{}", output);
}
Ok(())
}
}
fn filter_nothing(_instance: &InstanceWithMeta) -> bool {
true
}
fn filter_non_scripts(instance: &InstanceWithMeta) -> bool {
match instance.class_name() {
"Script" | "LocalScript" | "ModuleScript" => true,
_ => false,
}
}
fn recurse_create_node(
tree: &RojoTree,
referent: Ref,
project_dir: &Path,
filter: fn(&InstanceWithMeta) -> bool,
) -> Option<SourcemapNode> {
let instance = tree.get_instance(referent).expect("instance did not exist");
let mut children = Vec::new();
for &child_id in instance.children() {
if let Some(child_node) = recurse_create_node(tree, child_id, &project_dir, filter) {
children.push(child_node);
}
}
// If this object has no children and doesn't pass the filter, it doesn't
// contain any information we're looking for.
if children.is_empty() && !filter(&instance) {
return None;
}
let file_paths = instance
.metadata()
.relevant_paths
.iter()
// Not all paths listed as relevant are guaranteed to exist.
.filter(|path| path.is_file())
.map(|path| path.strip_prefix(project_dir).expect(PATH_STRIP_FAILED_ERR))
.map(|path| path.to_path_buf())
.collect();
Some(SourcemapNode {
name: instance.name().to_string(),
class_name: instance.class_name().to_string(),
file_paths,
children,
})
}

65
src/cli/unpack.rs Normal file
View File

@@ -0,0 +1,65 @@
use std::{io::BufReader, path::PathBuf};
use anyhow::bail;
use clap::Parser;
use fs_err::File;
use rbx_dom_weak::{Instance, WeakDom};
use crate::{Project, ProjectNode};
use super::resolve_path;
/// Unpack a Roblox place file into an existing Rojo project.
#[derive(Debug, Parser)]
pub struct UnpackCommand {
/// Path to the project to unpack. Defaults to the current directory.
#[clap(long, default_value = "")]
pub project: PathBuf,
/// Path to the place to unpack from.
pub place: PathBuf,
}
impl UnpackCommand {
pub fn run(self) -> anyhow::Result<()> {
let project_path = resolve_path(&self.project);
let project = match Project::load_fuzzy(&project_path)? {
Some(project) => project,
None => bail!("No project file was found; rojo unpack requires a project file."),
};
let place_ext = self
.place
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase());
let file = BufReader::new(File::open(&self.place)?);
let dom = match place_ext.as_deref() {
Some("rbxl") => rbx_binary::from_reader(file)?,
Some("rbxlx") => rbx_xml::from_reader_default(file)?,
Some(_) | None => bail!("Place files must end in .rbxl or .rbxlx"),
};
let context = Context { project, dom };
context.unpack();
Ok(())
}
}
struct Context {
project: Project,
dom: WeakDom,
}
impl Context {
fn unpack(&self) {
self.unpack_node(&self.project.tree, self.dom.root());
}
fn unpack_node(&self, node: &ProjectNode, instance: &Instance) {
// TODO
}
}

View File

@@ -2,30 +2,38 @@ use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{bail, format_err, Context};
use clap::Parser;
use memofs::Vfs;
use reqwest::{
header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT},
StatusCode,
};
use structopt::StructOpt;
use crate::{auth_cookie::get_auth_cookie, serve_session::ServeSession};
use super::resolve_path;
/// Builds the project and uploads it to Roblox.
#[derive(Debug, StructOpt)]
#[derive(Debug, Parser)]
pub struct UploadCommand {
/// Path to the project to upload. Defaults to the current directory.
#[structopt(default_value = "")]
#[clap(default_value = "")]
pub project: PathBuf,
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
#[structopt(long)]
#[clap(long)]
pub cookie: Option<String>,
/// API key obtained from create.roblox.com/credentials. Rojo will use the Open Cloud API when this is provided. Only supports uploading to a place.
#[clap(long = "api_key")]
pub api_key: Option<String>,
/// The Universe ID of the given place. Required when using the Open Cloud API.
#[clap(long = "universe_id")]
pub universe_id: Option<u64>,
/// Asset ID to upload to.
#[structopt(long = "asset_id")]
#[clap(long = "asset_id")]
pub asset_id: u64,
}
@@ -33,10 +41,6 @@ impl UploadCommand {
pub fn run(self) -> Result<(), anyhow::Error> {
let project_path = resolve_path(&self.project);
let cookie = self.cookie.or_else(get_auth_cookie).context(
"Rojo could not find your Roblox auth cookie. Please pass one via --cookie.",
)?;
let vfs = Vfs::new_default();
let session = ServeSession::new(vfs, project_path)?;
@@ -54,7 +58,36 @@ impl UploadCommand {
log::trace!("Encoding binary model");
rbx_binary::to_writer(&mut buffer, tree.inner(), &encode_ids)?;
do_upload(buffer, self.asset_id, &cookie)
match (self.cookie, self.api_key, self.universe_id) {
(cookie, None, universe) => {
// using legacy. notify if universe is provided.
if universe.is_some() {
log::warn!(
"--universe_id was provided but is ignored when using legacy upload"
);
}
let cookie = cookie.or_else(get_auth_cookie).context(
"Rojo could not find your Roblox auth cookie. Please pass one via --cookie.",
)?;
do_upload(buffer, self.asset_id, &cookie)
}
(cookie, Some(api_key), Some(universe_id)) => {
// using open cloud. notify if cookie is provided.
if cookie.is_some() {
log::warn!("--cookie was provided but is ignored when using Open Cloud API");
}
do_upload_open_cloud(buffer, universe_id, self.asset_id, &api_key)
}
(_, Some(_), None) => {
// API key is provided, universe id is not.
bail!("--universe_id must be provided to use the Open Cloud API");
}
}
}
}
@@ -90,7 +123,7 @@ fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()>
asset_id
);
let client = reqwest::Client::new();
let client = reqwest::blocking::Client::new();
let build_request = move || {
client
@@ -125,3 +158,38 @@ fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()>
Ok(())
}
/// Implementation of do_upload that supports the new open cloud api.
/// see https://developer.roblox.com/en-us/articles/open-cloud
fn do_upload_open_cloud(
buffer: Vec<u8>,
universe_id: u64,
asset_id: u64,
api_key: &str,
) -> anyhow::Result<()> {
let url = format!(
"https://apis.roblox.com/universes/v1/{}/places/{}/versions?versionType=Published",
universe_id, asset_id
);
let client = reqwest::blocking::Client::new();
log::debug!("Uploading to Roblox...");
let response = client
.post(&url)
.header("x-api-key", api_key)
.header(CONTENT_TYPE, "application/xml")
.header(ACCEPT, "application/json")
.body(buffer)
.send()?;
let status = response.status();
if !status.is_success() {
bail!(
"The Roblox API returned an unexpected error: {}",
response.text()?
);
}
Ok(())
}

View File

@@ -1,11 +1,14 @@
use std::{env, panic, process};
use backtrace::Backtrace;
use structopt::StructOpt;
use clap::Parser;
use librojo::cli::Options;
fn main() {
#[cfg(feature = "profile-with-tracy")]
tracy_client::Client::start();
panic::set_hook(Box::new(|panic_info| {
// PanicInfo's payload is usually a &'static str or String.
// See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload
@@ -49,7 +52,7 @@ fn main() {
process::exit(1);
}));
let options = Options::from_args();
let options = Options::parse();
let log_filter = match options.global.verbosity {
0 => "info",

View File

@@ -64,6 +64,7 @@ impl<T: Clone> MessageQueue<T> {
/// This method is only useful in tests. Non-test code should use subscribe
/// instead.
#[cfg(test)]
#[allow(unused)]
pub fn subscribe_any(&self) -> oneshot::Receiver<(u32, Vec<T>)> {
let cursor = {
let messages = self.messages.read().unwrap();

View File

@@ -36,17 +36,3 @@ where
seq.end()
}
pub fn serialize_option_absolute<S, T>(
maybe_path: &Option<T>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<Path>,
{
match maybe_path {
Some(path) => serialize_absolute(path, serializer),
None => serializer.serialize_none(),
}
}

View File

@@ -1,6 +1,7 @@
use std::{
collections::{BTreeMap, HashMap, HashSet},
fs, io,
net::IpAddr,
path::{Path, PathBuf},
};
@@ -67,6 +68,11 @@ pub struct Project {
#[serde(skip_serializing_if = "Option::is_none")]
pub game_id: Option<u64>,
/// If specified, this address will be used in place of the default address
/// As long as --address is unprovided.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_address: Option<IpAddr>,
/// 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")]
@@ -168,6 +174,35 @@ impl Project {
}
}
#[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 {
@@ -218,12 +253,8 @@ pub struct ProjectNode {
/// 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",
serialize_with = "crate::path_serializer::serialize_option_absolute",
skip_serializing_if = "Option::is_none"
)]
pub path: Option<PathBuf>,
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
pub path: Option<PathNode>,
}
impl ProjectNode {
@@ -243,3 +274,106 @@ impl ProjectNode {
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn path_node_required() {
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
}
#[test]
fn path_node_optional() {
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
assert_eq!(
path_node,
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
);
}
#[test]
fn project_node_required() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "src"
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Required(PathBuf::from("src")))
);
}
#[test]
fn project_node_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "src" }
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Optional(OptionalPathNode::new(PathBuf::from(
"src"
))))
);
}
#[test]
fn project_node_none() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$className": "Folder"
}"#,
)
.unwrap();
assert_eq!(project_node.path, None);
}
#[test]
fn project_node_optional_serialize_absolute() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "..\\src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_absolute_no_change() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "../src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "..\\src"
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":"../src"}"#);
}
}

View File

@@ -1,7 +1,9 @@
use std::borrow::Borrow;
use anyhow::format_err;
use rbx_dom_weak::types::{Color3, Content, Enum, Tags, Variant, VariantType, Vector2, Vector3};
use rbx_dom_weak::types::{
CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3,
};
use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize};
@@ -37,6 +39,7 @@ pub enum AmbiguousValue {
Array2([f64; 2]),
Array3([f64; 3]),
Array4([f64; 4]),
Array12([f64; 12]),
}
impl AmbiguousValue {
@@ -113,6 +116,18 @@ impl AmbiguousValue {
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,
@@ -138,6 +153,7 @@ impl AmbiguousValue {
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",
}
}
}

View File

@@ -2,6 +2,7 @@ use std::{
borrow::Cow,
collections::HashSet,
io,
net::IpAddr,
path::{Path, PathBuf},
sync::{Arc, Mutex, MutexGuard},
time::Instant,
@@ -126,11 +127,10 @@ impl ServeSession {
let instance_context = InstanceContext::default();
log::trace!("Generating snapshot of instances from VFS");
let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?
.expect("snapshot did not return an instance");
let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?;
log::trace!("Computing initial patch set");
let patch_set = compute_patch_set(&snapshot, &tree, root_id);
let patch_set = compute_patch_set(snapshot, &tree, root_id);
log::trace!("Applying initial patch set");
apply_patch_set(&mut tree, patch_set);
@@ -212,6 +212,14 @@ impl ServeSession {
pub fn serve_place_ids(&self) -> Option<&HashSet<u64>> {
self.root_project.serve_place_ids.as_ref()
}
pub fn serve_address(&self) -> Option<IpAddr> {
self.root_project.serve_address
}
pub fn root_dir(&self) -> &Path {
self.root_project.folder_location()
}
}
#[derive(Debug, Error)]

View File

@@ -4,7 +4,7 @@ use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{
types::{Ref, Variant},
WeakDom,
Instance, WeakDom,
};
use serde::{Deserialize, Serialize};
@@ -102,22 +102,29 @@ impl InstanceSnapshot {
}
}
pub fn from_tree(tree: &WeakDom, id: Ref) -> Self {
let instance = tree.get_by_ref(id).expect("instance did not exist in tree");
#[profiling::function]
pub fn from_tree(tree: WeakDom, id: Ref) -> Self {
let (_, mut raw_tree) = tree.into_raw();
Self::from_raw_tree(&mut raw_tree, id)
}
fn from_raw_tree(raw_tree: &mut HashMap<Ref, Instance>, id: Ref) -> Self {
let instance = raw_tree
.remove(&id)
.expect("instance did not exist in tree");
let children = instance
.children()
.iter()
.copied()
.map(|id| Self::from_tree(tree, id))
.map(|&id| Self::from_raw_tree(raw_tree, id))
.collect();
Self {
snapshot_id: Some(id),
metadata: InstanceMetadata::default(),
name: Cow::Owned(instance.name.clone()),
class_name: Cow::Owned(instance.class.clone()),
properties: instance.properties.clone(),
name: Cow::Owned(instance.name),
class_name: Cow::Owned(instance.class),
properties: instance.properties,
children,
}
}

View File

@@ -1,6 +1,9 @@
//! Defines the algorithm for applying generated patches.
use std::collections::HashMap;
use std::{
collections::{HashMap, HashSet},
mem::take,
};
use rbx_dom_weak::types::{Ref, Variant};
@@ -12,21 +15,31 @@ use super::{
/// Consumes the input `PatchSet`, applying all of its prescribed changes to the
/// tree and returns an `AppliedPatchSet`, which can be used to keep another
/// tree in sync with Rojo's.
#[profiling::function]
pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatchSet {
let mut context = PatchApplyContext::default();
for removed_id in patch_set.removed_instances {
apply_remove_instance(&mut context, tree, removed_id);
{
profiling::scope!("removals");
for removed_id in patch_set.removed_instances {
apply_remove_instance(&mut context, tree, removed_id);
}
}
for add_patch in patch_set.added_instances {
apply_add_child(&mut context, tree, add_patch.parent_id, add_patch.instance);
{
profiling::scope!("additions");
for add_patch in patch_set.added_instances {
apply_add_child(&mut context, tree, add_patch.parent_id, add_patch.instance);
}
}
// Updates need to be applied after additions, which reduces the complexity
// of updates significantly.
for update_patch in patch_set.updated_instances {
apply_update_child(&mut context, tree, update_patch);
{
profiling::scope!("updates");
// Updates need to be applied after additions, which reduces the complexity
// of updates significantly.
for update_patch in patch_set.updated_instances {
apply_update_child(&mut context, tree, update_patch);
}
}
finalize_patch_application(context, tree)
@@ -55,20 +68,9 @@ struct PatchApplyContext {
/// eachother.
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
/// The properties of instances added by the current `PatchSet`.
///
/// Instances added to the tree can refer to eachother via Ref properties,
/// but we need to make sure they're correctly transformed from snapshot
/// space into tree space (via `snapshot_id_to_instance_id`).
///
/// It's not possible to do that transformation for refs that refer to added
/// instances until all the instances have actually been inserted into the
/// tree. For simplicity, we defer application of _all_ properties on added
/// instances instead of just Refs.
///
/// This doesn't affect updated instances, since they're always applied
/// after we've added all the instances from the patch.
added_instance_properties: HashMap<Ref, HashMap<String, Variant>>,
/// Tracks all of the instances added by this patch that have refs that need
/// to be rewritten.
has_refs_to_rewrite: HashSet<Ref>,
/// The current applied patch result, describing changes made to the tree.
applied_patch_set: AppliedPatchSet,
@@ -84,23 +86,22 @@ struct PatchApplyContext {
/// The remaining Ref properties need to be handled during patch application,
/// where we build up a map of snapshot IDs to instance IDs as they're created,
/// then apply properties all at once at the end.
#[profiling::function]
fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -> AppliedPatchSet {
for (id, properties) in context.added_instance_properties {
for id in context.has_refs_to_rewrite {
// This should always succeed since instances marked as added in our
// patch should be added without fail.
let mut instance = tree
.get_instance_mut(id)
.expect("Invalid instance ID in deferred property map");
for (key, mut property_value) in properties {
if let Variant::Ref(referent) = property_value {
for value in instance.properties_mut().values_mut() {
if let Variant::Ref(referent) = value {
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(&referent)
{
property_value = Variant::Ref(instance_referent);
*value = Variant::Ref(instance_referent);
}
}
instance.properties_mut().insert(key, property_value);
}
}
@@ -116,24 +117,24 @@ fn apply_add_child(
context: &mut PatchApplyContext,
tree: &mut RojoTree,
parent_id: Ref,
snapshot: InstanceSnapshot,
mut snapshot: InstanceSnapshot,
) {
let snapshot_id = snapshot.snapshot_id;
let properties = snapshot.properties;
let children = snapshot.children;
let children = take(&mut snapshot.children);
// Property application is deferred until after all children
// are constructed. This helps apply referents correctly.
let remaining_snapshot = InstanceSnapshot::new()
.name(snapshot.name)
.class_name(snapshot.class_name)
.metadata(snapshot.metadata)
.snapshot_id(snapshot.snapshot_id);
// If an object we're adding has a non-null referent, we'll note this
// instance down as needing to be revisited later.
let has_refs = snapshot.properties.values().any(|value| match value {
Variant::Ref(value) => value.is_some(),
_ => false,
});
let id = tree.insert_instance(parent_id, remaining_snapshot);
let id = tree.insert_instance(parent_id, snapshot);
context.applied_patch_set.added.push(id);
context.added_instance_properties.insert(id, properties);
if has_refs {
context.has_refs_to_rewrite.insert(id);
}
if let Some(snapshot_id) = snapshot_id {
context.snapshot_id_to_instance_id.insert(snapshot_id, id);

View File

@@ -1,7 +1,10 @@
//! Defines the algorithm for computing a roughly-minimal patch set given an
//! existing instance tree and an instance snapshot.
use std::collections::{HashMap, HashSet};
use std::{
collections::{HashMap, HashSet},
mem::take,
};
use rbx_dom_weak::types::{Ref, Variant};
@@ -10,16 +13,24 @@ use super::{
InstanceSnapshot, InstanceWithMeta, RojoTree,
};
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref) -> PatchSet {
#[profiling::function]
pub fn compute_patch_set(snapshot: Option<InstanceSnapshot>, tree: &RojoTree, id: Ref) -> PatchSet {
let mut patch_set = PatchSet::new();
let mut context = ComputePatchContext::default();
compute_patch_set_internal(&mut context, snapshot, tree, id, &mut patch_set);
if let Some(snapshot) = snapshot {
let mut context = ComputePatchContext::default();
// Rewrite Ref properties to refer to instance IDs instead of snapshot IDs
// for all of the IDs that we know about so far.
rewrite_refs_in_updates(&context, &mut patch_set.updated_instances);
rewrite_refs_in_additions(&context, &mut patch_set.added_instances);
compute_patch_set_internal(&mut context, snapshot, tree, id, &mut patch_set);
// Rewrite Ref properties to refer to instance IDs instead of snapshot IDs
// for all of the IDs that we know about so far.
rewrite_refs_in_updates(&context, &mut patch_set.updated_instances);
rewrite_refs_in_additions(&context, &mut patch_set.added_instances);
} else {
if id != tree.get_root_id() {
patch_set.removed_instances.push(id);
}
}
patch_set
}
@@ -63,7 +74,7 @@ fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut Instan
fn compute_patch_set_internal(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
mut snapshot: InstanceSnapshot,
tree: &RojoTree,
id: Ref,
patch_set: &mut PatchSet,
@@ -76,12 +87,12 @@ fn compute_patch_set_internal(
.get_instance(id)
.expect("Instance did not exist in tree");
compute_property_patches(snapshot, &instance, patch_set);
compute_children_patches(context, snapshot, tree, id, patch_set);
compute_property_patches(&mut snapshot, &instance, patch_set);
compute_children_patches(context, &mut snapshot, tree, id, patch_set);
}
fn compute_property_patches(
snapshot: &InstanceSnapshot,
snapshot: &mut InstanceSnapshot,
instance: &InstanceWithMeta,
patch_set: &mut PatchSet,
) {
@@ -91,32 +102,32 @@ fn compute_property_patches(
let changed_name = if snapshot.name == instance.name() {
None
} else {
Some(snapshot.name.clone().into_owned())
Some(take(&mut snapshot.name).into_owned())
};
let changed_class_name = if snapshot.class_name == instance.class_name() {
None
} else {
Some(snapshot.class_name.clone().into_owned())
Some(take(&mut snapshot.class_name).into_owned())
};
let changed_metadata = if &snapshot.metadata == instance.metadata() {
None
} else {
Some(snapshot.metadata.clone())
Some(take(&mut snapshot.metadata))
};
for (name, snapshot_value) in &snapshot.properties {
visited_properties.insert(name.as_str());
for (name, snapshot_value) in take(&mut snapshot.properties) {
visited_properties.insert(name.clone());
match instance.properties().get(name) {
match instance.properties().get(&name) {
Some(instance_value) => {
if snapshot_value != instance_value {
changed_properties.insert(name.clone(), Some(snapshot_value.clone()));
if &snapshot_value != instance_value {
changed_properties.insert(name, Some(snapshot_value));
}
}
None => {
changed_properties.insert(name.clone(), Some(snapshot_value.clone()));
changed_properties.insert(name, Some(snapshot_value));
}
}
}
@@ -148,7 +159,7 @@ fn compute_property_patches(
fn compute_children_patches(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
snapshot: &mut InstanceSnapshot,
tree: &RojoTree,
id: Ref,
patch_set: &mut PatchSet,
@@ -161,7 +172,7 @@ fn compute_children_patches(
let mut paired_instances = vec![false; instance_children.len()];
for snapshot_child in snapshot.children.iter() {
for snapshot_child in take(&mut snapshot.children) {
let matching_instance =
instance_children
.iter()
@@ -198,7 +209,7 @@ fn compute_children_patches(
None => {
patch_set.added_instances.push(PatchAdd {
parent_id: id,
instance: snapshot_child.clone(),
instance: snapshot_child,
});
}
}
@@ -246,7 +257,7 @@ mod test {
children: Vec::new(),
};
let patch_set = compute_patch_set(&snapshot, &tree, root_id);
let patch_set = compute_patch_set(Some(snapshot), &tree, root_id);
let expected_patch_set = PatchSet {
updated_instances: vec![PatchUpdate {
@@ -296,7 +307,7 @@ mod test {
class_name: Cow::Borrowed("foo"),
};
let patch_set = compute_patch_set(&snapshot, &tree, root_id);
let patch_set = compute_patch_set(Some(snapshot), &tree, root_id);
let expected_patch_set = PatchSet {
added_instances: vec![PatchAdd {

View File

@@ -23,7 +23,7 @@ fn set_name_and_class_name() {
children: Vec::new(),
};
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
@@ -47,7 +47,7 @@ fn set_property() {
children: Vec::new(),
};
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
@@ -78,7 +78,7 @@ fn remove_property() {
children: Vec::new(),
};
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
@@ -107,7 +107,7 @@ fn add_child() {
}],
};
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
@@ -139,7 +139,7 @@ fn remove_child() {
children: Vec::new(),
};
let patch_set = compute_patch_set(&snapshot, &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);

View File

@@ -87,8 +87,9 @@ impl RojoTree {
}
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
let builder = InstanceBuilder::new(snapshot.class_name.to_owned())
.with_name(snapshot.name.to_owned())
let builder = InstanceBuilder::empty()
.with_class(snapshot.class_name.into_owned())
.with_name(snapshot.name.into_owned())
.with_properties(snapshot.properties);
let referent = self.inner.insert(parent_ref, builder);

View File

@@ -10,6 +10,40 @@ pub fn snapshot_dir(
context: &InstanceContext,
vfs: &Vfs,
path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let mut snapshot = match snapshot_dir_no_meta(context, vfs, path)? {
Some(snapshot) => snapshot,
None => return Ok(None),
};
if let Some(mut meta) = dir_meta(vfs, path)? {
meta.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))
}
/// Retrieves the meta file that should be applied for this directory, if it
/// exists.
pub fn dir_meta(vfs: &Vfs, path: &Path) -> anyhow::Result<Option<DirectoryMetadata>> {
let meta_path = path.join("init.meta.json");
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?;
Ok(Some(metadata))
} else {
Ok(None)
}
}
/// Snapshot a directory without applying meta files; useful for if the
/// directory's ClassName will change before metadata should be applied. For
/// example, this can happen if the directory contains an `init.client.lua`
/// file.
pub fn snapshot_dir_no_meta(
context: &InstanceContext,
vfs: &Vfs,
path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let passes_filter_rules = |child: &DirEntry| {
context
@@ -52,7 +86,7 @@ pub fn snapshot_dir(
path.join("init.client.lua"),
];
let mut snapshot = InstanceSnapshot::new()
let snapshot = InstanceSnapshot::new()
.name(instance_name)
.class_name("Folder")
.children(snapshot_children)
@@ -63,11 +97,6 @@ pub fn snapshot_dir(
.context(context),
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))
}

View File

@@ -6,7 +6,11 @@ use memofs::{IoResultExt, Vfs};
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::{dir::snapshot_dir, meta_file::AdjacentMetadata, util::match_trailing};
use super::{
dir::{dir_meta, snapshot_dir_no_meta},
meta_file::AdjacentMetadata,
util::match_trailing,
};
/// Core routine for turning Lua files into snapshots.
pub fn snapshot_lua(
@@ -66,14 +70,14 @@ pub fn snapshot_lua_init(
init_path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let folder_path = init_path.parent().unwrap();
let dir_snapshot = snapshot_dir(context, vfs, folder_path)?.unwrap();
let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap();
if dir_snapshot.class_name != "Folder" {
anyhow::bail!(
"init.lua, init.server.lua, and init.client.lua can \
only be used if the instance produced by the containing \
directory would be a Folder.\n\n\
directory would be a Folder.\n\
\n\
The directory {} turned into an instance of class {}.",
folder_path.display(),
dir_snapshot.class_name
@@ -86,6 +90,10 @@ pub fn snapshot_lua_init(
init_snapshot.children = dir_snapshot.children;
init_snapshot.metadata = dir_snapshot.metadata;
if let Some(mut meta) = dir_meta(vfs, folder_path)? {
meta.apply_all(&mut init_snapshot)?;
}
Ok(Some(init_snapshot))
}

View File

@@ -40,6 +40,7 @@ pub use self::project::snapshot_project_node;
/// The main entrypoint to the snapshot function. This function can be pointed
/// at any path and will return something if Rojo knows how to deal with it.
#[profiling::function]
pub fn snapshot_from_vfs(
context: &InstanceContext,
vfs: &Vfs,

View File

@@ -5,7 +5,7 @@ use memofs::Vfs;
use rbx_reflection::ClassTag;
use crate::{
project::{Project, ProjectNode},
project::{PathNode, Project, ProjectNode},
snapshot::{
InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule,
},
@@ -30,30 +30,31 @@ pub fn snapshot_project(
context.add_path_ignore_rules(rules);
// TODO: If this project node is a path to an instance that Rojo doesn't
// understand, this may panic!
let mut snapshot =
snapshot_project_node(&context, path, &project.name, &project.tree, vfs, None)?.unwrap();
match snapshot_project_node(&context, path, &project.name, &project.tree, vfs, None)? {
Some(found_snapshot) => {
let mut snapshot = found_snapshot;
// Setting the instigating source to the project file path is a little
// coarse.
//
// Ideally, we'd only snapshot the project file if the project file
// actually changed. Because Rojo only has the concept of one
// relevant path -> snapshot path mapping per instance, we pick the more
// conservative approach of snapshotting the project file if any
// relevant paths changed.
snapshot.metadata.instigating_source = Some(path.to_path_buf().into());
// Setting the instigating source to the project file path is a little
// coarse.
//
// Ideally, we'd only snapshot the project file if the project file
// actually changed. Because Rojo only has the concept of one
// relevant path -> snapshot path mapping per instance, we pick the more
// conservative approach of snapshotting the project file if any
// relevant paths changed.
snapshot.metadata.instigating_source = Some(path.to_path_buf().into());
// Mark this snapshot (the root node of the project file) as being
// related to the project file.
//
// We SHOULD NOT mark the project file as a relevant path for any
// nodes that aren't roots. They'll be updated as part of the project
// file being updated.
snapshot.metadata.relevant_paths.push(path.to_path_buf());
// Mark this snapshot (the root node of the project file) as being
// related to the project file.
//
// We SHOULD NOT mark the project file as a relevant path for any
// nodes that aren't roots. They'll be updated as part of the project
// file being updated.
snapshot.metadata.relevant_paths.push(path.to_path_buf());
Ok(Some(snapshot))
Ok(Some(snapshot))
}
None => Ok(None),
}
}
pub fn snapshot_project_node(
@@ -77,16 +78,18 @@ pub fn snapshot_project_node(
let mut children = Vec::new();
let mut metadata = InstanceMetadata::default();
if let Some(path) = &node.path {
if let Some(path_node) = &node.path {
let path = path_node.path();
// If the path specified in the project is relative, we assume it's
// relative to the folder that the project is in, project_folder.
let path = if path.is_relative() {
let full_path = if path.is_relative() {
Cow::Owned(project_folder.join(path))
} else {
Cow::Borrowed(path)
};
if let Some(snapshot) = snapshot_from_vfs(context, vfs, &path)? {
if let Some(snapshot) = snapshot_from_vfs(context, vfs, &full_path)? {
class_name_from_path = Some(snapshot.class_name);
// Properties from the snapshot are pulled in unchanged, and
@@ -106,11 +109,6 @@ pub fn snapshot_project_node(
// Take the snapshot's metadata as-is, which will be mutated later
// on.
metadata = snapshot.metadata;
} else {
// TODO: Should this issue an error instead?
log::warn!(
"$path referred to a path that could not be turned into an instance by Rojo"
);
}
}
@@ -120,20 +118,21 @@ pub fn snapshot_project_node(
class_name_from_project,
class_name_from_path,
class_name_from_inference,
&node.path,
) {
// These are the easy, happy paths!
(Some(project), None, None) => project,
(None, Some(path), None) => path,
(None, None, Some(inference)) => inference,
(Some(project), None, None, _) => project,
(None, Some(path), None, _) => path,
(None, None, Some(inference), _) => inference,
// If the user specifies a class name, but there's an inferred class
// name, we prefer the name listed explicitly by the user.
(Some(project), None, Some(_)) => project,
(Some(project), None, Some(_), _) => project,
// If the user has a $path pointing to a folder and we're able to infer
// a class name, let's use the inferred name. If the path we're pointing
// to isn't a folder, though, that's a user error.
(None, Some(path), Some(inference)) => {
(None, Some(path), Some(inference), _) => {
if path == "Folder" {
inference
} else {
@@ -141,7 +140,7 @@ pub fn snapshot_project_node(
}
}
(Some(project), Some(path), _) => {
(Some(project), Some(path), _, _) => {
if path == "Folder" {
project
} else {
@@ -155,12 +154,28 @@ pub fn snapshot_project_node(
project,
path,
project_path.display(),
node.path.as_ref().unwrap().display()
node.path.as_ref().unwrap().path().display()
);
}
}
(None, None, None) => {
(None, None, None, Some(PathNode::Optional(_))) => {
return Ok(None);
}
(_, None, _, Some(PathNode::Required(path))) => {
anyhow::bail!(
"Rojo project referred to a file using $path that could not be turned into a Roblox Instance by Rojo.\n\
Check that the file exists and is a file type known by Rojo.\n\
\n\
Project path: {}\n\
File $path: {}",
project_path.display(),
path.display(),
);
}
(None, None, None, None) => {
bail!(
"Instance \"{}\" is missing some required information.\n\
One of the following must be true:\n\

View File

@@ -7,6 +7,7 @@ use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::util::PathExt;
#[profiling::function]
pub fn snapshot_rbxm(
context: &InstanceContext,
vfs: &Vfs,
@@ -21,7 +22,8 @@ pub fn snapshot_rbxm(
let children = root_instance.children();
if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])
let child = children[0];
let snapshot = InstanceSnapshot::from_tree(temp_tree, child)
.name(name)
.metadata(
InstanceMetadata::new()

View File

@@ -24,7 +24,8 @@ pub fn snapshot_rbxmx(
let children = root_instance.children();
if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])
let child = children[0];
let snapshot = InstanceSnapshot::from_tree(temp_tree, child)
.name(name)
.metadata(
InstanceMetadata::new()

View File

@@ -11,9 +11,9 @@ use crate::{
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
web::{
interface::{
ErrorResponse, Instance, InstanceMetadata as WebInstanceMetadata, InstanceUpdate,
OpenResponse, ReadResponse, ServerInfoResponse, SubscribeMessage, SubscribeResponse,
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
ErrorResponse, Instance, OpenResponse, ReadResponse, ServerInfoResponse,
SubscribeMessage, SubscribeResponse, WriteRequest, WriteResponse, PROTOCOL_VERSION,
SERVER_VERSION,
},
util::{json, json_ok},
},
@@ -99,44 +99,7 @@ impl ApiService {
let api_messages = messages
.into_iter()
.map(|message| {
let removed = message.removed;
let mut added = HashMap::new();
for id in message.added {
let instance = tree.get_instance(id).unwrap();
added.insert(id, Instance::from_rojo_instance(instance));
for instance in tree.descendants(id) {
added.insert(instance.id(), Instance::from_rojo_instance(instance));
}
}
let updated = message
.updated
.into_iter()
.map(|update| {
let changed_metadata = update
.changed_metadata
.as_ref()
.map(WebInstanceMetadata::from_rojo_metadata);
InstanceUpdate {
id: update.id,
changed_name: update.changed_name,
changed_class_name: update.changed_class_name,
changed_properties: update.changed_properties,
changed_metadata,
}
})
.collect();
SubscribeMessage {
removed,
added,
updated,
}
})
.map(|patch| SubscribeMessage::from_patch_update(&tree, patch))
.collect();
json_ok(SubscribeResponse {

View File

@@ -7,12 +7,14 @@ use std::{
collections::{HashMap, HashSet},
};
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::types::{Ref, Variant, VariantType};
use serde::{Deserialize, Serialize};
use crate::{
session_id::SessionId,
snapshot::{InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta},
snapshot::{
AppliedPatchSet, InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta, RojoTree,
},
};
/// Server version to report over the API, not exposed outside this crate.
@@ -30,6 +32,53 @@ pub struct SubscribeMessage<'a> {
pub updated: Vec<InstanceUpdate>,
}
impl<'a> SubscribeMessage<'a> {
pub(crate) fn from_patch_update(tree: &'a RojoTree, patch: AppliedPatchSet) -> Self {
let removed = patch.removed;
let mut added = HashMap::new();
for id in patch.added {
let instance = tree.get_instance(id).unwrap();
added.insert(id, Instance::from_rojo_instance(instance));
for instance in tree.descendants(id) {
added.insert(instance.id(), Instance::from_rojo_instance(instance));
}
}
let updated = patch
.updated
.into_iter()
.map(|update| {
let changed_metadata = update
.changed_metadata
.as_ref()
.map(InstanceMetadata::from_rojo_metadata);
let changed_properties = update
.changed_properties
.into_iter()
.filter(|(_key, value)| property_filter(value.as_ref()))
.collect();
InstanceUpdate {
id: update.id,
changed_name: update.changed_name,
changed_class_name: update.changed_class_name,
changed_properties,
changed_metadata,
}
})
.collect();
Self {
removed,
added,
updated,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstanceUpdate {
@@ -75,14 +124,8 @@ impl<'a> Instance<'a> {
let properties = source
.properties()
.iter()
.filter_map(|(key, value)| {
// SharedString values can't be serialized via Serde
if matches!(value, Variant::SharedString(_)) {
return None;
}
Some((key.clone(), Cow::Borrowed(value)))
})
.filter(|(_key, value)| property_filter(Some(value)))
.map(|(key, value)| (key.clone(), Cow::Borrowed(value)))
.collect();
Instance {
@@ -97,6 +140,18 @@ impl<'a> Instance<'a> {
}
}
fn property_filter(value: Option<&Variant>) -> bool {
let ty = value.map(|value| value.ty());
// Lua can't do anything with SharedString values. They also can't be
// serialized directly by Serde!
if ty == Some(VariantType::SharedString) {
return false;
}
return true;
}
/// Response body from /api/rojo
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]

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