Compare commits

...

37 Commits

Author SHA1 Message Date
Lucien Greathouse
66ef335172 ...and build with submodules for other builds too 2022-05-25 19:42:40 -04:00
Lucien Greathouse
4352590ba4 Checkout submodules in plugin build step 2022-05-25 19:40:42 -04:00
Lucien Greathouse
0fe751ef33 Port release workflow from Aftman to test 2022-05-25 19:36:54 -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
63 changed files with 3366 additions and 1663 deletions

View File

@@ -3,37 +3,53 @@ name: CI
on:
push:
branches:
- master
- main
pull_request:
branches:
- master
- main
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
with:
submodules: true
- uses: actions/checkout@v3
- 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
- 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

@@ -2,6 +2,34 @@
## Unreleased Changes
## [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 +103,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:
@@ -51,4 +56,4 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
* 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
* Attach release artifacts from GitHub Actions for each platform

1611
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.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0"
@@ -69,15 +69,12 @@ 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"
@@ -85,10 +82,10 @@ 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"] }
[target.'cfg(windows)'.dependencies]
winreg = "0.9.0"
winreg = "0.10.1"
[build-dependencies]
memofs = { version = "0.2.0", path = "memofs" }
@@ -104,9 +101,8 @@ rojo-insta-ext = { path = "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

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, 0},
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,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,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.as_ref(), &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.as_ref(), &tree, id);
apply_patch_set(tree, patch_set)
}
};

View File

@@ -6,6 +6,7 @@ mod fmt_project;
mod init;
mod plugin;
mod serve;
mod sourcemap;
mod upload;
use std::{borrow::Cow, env, path::Path, str::FromStr};
@@ -19,6 +20,7 @@ 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::upload::UploadCommand;
/// Command line options that Rojo accepts, defined using the structopt crate.
@@ -40,6 +42,7 @@ 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(),
@@ -112,6 +115,7 @@ pub enum Subcommand {
Serve(ServeCommand),
Build(BuildCommand),
Upload(UploadCommand),
Sourcemap(SourcemapCommand),
FmtProject(FmtProjectCommand),
Doc(DocCommand),
Plugin(PluginCommand),

View File

@@ -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 fs_err::File;
use memofs::Vfs;
use rbx_dom_weak::types::Ref;
use serde::Serialize;
use structopt::StructOpt;
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, StructOpt)]
pub struct SourcemapCommand {
/// Path to the project to use for the sourcemap. Defaults to the current
/// directory.
#[structopt(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.
#[structopt(long, short)]
pub output: Option<PathBuf>,
/// If non-script files should be included or not. Defaults to false.
#[structopt(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,
})
}

View File

@@ -24,6 +24,14 @@ pub struct UploadCommand {
#[structopt(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.
#[structopt(long = "api_key")]
pub api_key: Option<String>,
/// The Universe ID of the given place. Required when using the Open Cloud API.
#[structopt(long = "universe_id")]
pub universe_id: Option<u64>,
/// Asset ID to upload to.
#[structopt(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

@@ -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.as_ref(), &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

@@ -10,16 +10,27 @@ use super::{
InstanceSnapshot, InstanceWithMeta, RojoTree,
};
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref) -> PatchSet {
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
}
@@ -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

@@ -72,8 +72,8 @@ pub fn snapshot_lua_init(
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

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

@@ -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")]

View File

@@ -1,12 +0,0 @@
{
"name": "weldconstraint",
"tree": {
"$className": "DataModel",
"Workspace": {
"Parts": {
"$path": "two-parts-welded.rbxmx"
}
}
}
}

66
testez.toml Normal file
View File

@@ -0,0 +1,66 @@
[[afterAll.args]]
type = "function"
[[afterEach.args]]
type = "function"
[[beforeAll.args]]
type = "function"
[[beforeEach.args]]
type = "function"
[[describe.args]]
type = "string"
[[describe.args]]
type = "function"
[[describeFOCUS.args]]
type = "string"
[[describeFOCUS.args]]
type = "function"
[[describeSKIP.args]]
type = "string"
[[describeSKIP.args]]
type = "function"
[[expect.args]]
type = "any"
[[FIXME.args]]
type = "string"
required = false
[FOCUS]
args = []
[[it.args]]
type = "string"
[[it.args]]
type = "function"
[[itFIXME.args]]
type = "string"
[[itFIXME.args]]
type = "function"
[[itFOCUS.args]]
type = "string"
[[itFOCUS.args]]
type = "function"
[[itSKIP.args]]
type = "string"
[[itSKIP.args]]
type = "function"
[SKIP]
args = []

View File

@@ -1,5 +1,6 @@
use std::{
fs, io,
fs,
io::{self, Read},
path::{Path, PathBuf},
process::Child,
};
@@ -50,5 +51,17 @@ pub struct KillOnDrop(pub Child);
impl Drop for KillOnDrop {
fn drop(&mut self) {
let _ = self.0.kill();
if let Some(mut stdout) = self.0.stdout.take() {
let mut output = Vec::new();
let _ = stdout.read_to_end(&mut output);
print!("{}", String::from_utf8_lossy(&output));
}
if let Some(mut stderr) = self.0.stderr.take() {
let mut output = Vec::new();
let _ = stderr.read_to_end(&mut output);
eprint!("{}", String::from_utf8_lossy(&output));
}
}
}

View File

@@ -141,14 +141,14 @@ impl TestServeSession {
pub fn get_api_rojo(&self) -> Result<ServerInfoResponse, reqwest::Error> {
let url = format!("http://localhost:{}/api/rojo", self.port);
let body = reqwest::get(&url)?.text()?;
let body = reqwest::blocking::get(&url)?.text()?;
Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
}
pub fn get_api_read(&self, id: Ref) -> Result<ReadResponse, reqwest::Error> {
let url = format!("http://localhost:{}/api/read/{}", self.port, id);
let body = reqwest::get(&url)?.text()?;
let body = reqwest::blocking::get(&url)?.text()?;
Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
}
@@ -159,7 +159,7 @@ impl TestServeSession {
) -> Result<SubscribeResponse<'static>, reqwest::Error> {
let url = format!("http://localhost:{}/api/subscribe/{}", self.port, cursor);
reqwest::get(&url)?.json()
reqwest::blocking::get(&url)?.json()
}
}

View File

@@ -41,6 +41,9 @@ gen_build_tests! {
json_model_legacy_name,
module_in_folder,
module_init,
project_composed_default,
project_composed_file,
project_root_name,
rbxm_in_folder,
rbxmx_in_folder,
rbxmx_ref,
@@ -50,6 +53,8 @@ gen_build_tests! {
txt,
txt_in_folder,
unresolved_values,
optional,
weldconstraint,
}
fn run_build_test(test_name: &str) {
@@ -60,7 +65,7 @@ fn run_build_test(test_name: &str) {
let output_dir = tempdir().expect("couldn't create temporary directory");
let output_path = output_dir.path().join(format!("{}.rbxmx", test_name));
let status = Command::new(ROJO_PATH)
let output = Command::new(ROJO_PATH)
.args(&[
"build",
input_path.to_str().unwrap(),
@@ -69,10 +74,13 @@ fn run_build_test(test_name: &str) {
])
.env("RUST_LOG", "error")
.current_dir(working_dir)
.status()
.output()
.expect("Couldn't start Rojo");
assert!(status.success(), "Rojo did not exit successfully");
print!("{}", String::from_utf8_lossy(&output.stdout));
eprint!("{}", String::from_utf8_lossy(&output.stderr));
assert!(output.status.success(), "Rojo did not exit successfully");
let contents = fs::read_to_string(&output_path).expect("Couldn't read output file");

View File

@@ -220,3 +220,34 @@ fn empty_json_model() {
);
});
}
#[test]
#[ignore = "Rojo does not watch missing, optional files for changes."]
fn add_optional_folder() {
run_serve_test("add_optional_folder", |session, mut redactions| {
let info = session.get_api_rojo().unwrap();
let root_id = info.root_instance_id;
assert_yaml_snapshot!("add_optional_folder", redactions.redacted_yaml(info));
let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!(
"add_optional_folder_all",
read_response.intern_and_redact(&mut redactions, root_id)
);
fs::create_dir(session.path().join("create-later")).unwrap();
let subscribe_response = session.get_api_subscribe(0).unwrap();
assert_yaml_snapshot!(
"add_optional_folder_subscribe",
subscribe_response.intern_and_redact(&mut redactions, ())
);
let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!(
"add_optional_folder_all-2",
read_response.intern_and_redact(&mut redactions, root_id)
);
});
}