forked from rojo-rbx/rojo
Compare commits
26 Commits
v7.0.0-rc.
...
v7.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5a69fd9fc | ||
|
|
f1d0f1c1c9 | ||
|
|
83492d7495 | ||
|
|
10abc2254a | ||
|
|
5d5536a95e | ||
|
|
fe81e55925 | ||
|
|
654690d73e | ||
|
|
256aba4bc1 | ||
|
|
49f8845105 | ||
|
|
12370846b4 | ||
|
|
07637dfe96 | ||
|
|
f389a4a1db | ||
|
|
af077c796c | ||
|
|
1c319f2fa8 | ||
|
|
e8afa03f7b | ||
|
|
9b22545842 | ||
|
|
adc733d25c | ||
|
|
6896257647 | ||
|
|
1d9845a6cb | ||
|
|
8461339e9a | ||
|
|
9904d94e4c | ||
|
|
da25c80d0b | ||
|
|
5fa63733fd | ||
|
|
8b54bf0ba1 | ||
|
|
173dc12cb3 | ||
|
|
e136529ff0 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
rust_version: [stable, "1.46.0"]
|
||||
rust_version: [stable, "1.55.0"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
787
Cargo.lock
generated
787
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
BIN
assets/icon-link-32.png
Normal file
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
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
40
plugin/src/App/Components/Studio/StudioPluginAction.lua
Normal file
40
plugin/src/App/Components/Studio/StudioPluginAction.lua
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -136,22 +182,22 @@ function App:render()
|
||||
})
|
||||
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,
|
||||
NotConnectedPage = createPageElement(AppStatus.NotConnected, {
|
||||
host = self.host,
|
||||
onHostChange = self.setHost,
|
||||
port = self.port,
|
||||
onPortChange = self.setPort,
|
||||
|
||||
onNavigateSettings = function()
|
||||
self:setState({
|
||||
appStatus = AppStatus.Settings,
|
||||
})
|
||||
end,
|
||||
})
|
||||
end),
|
||||
onConnect = function()
|
||||
self:startSession()
|
||||
end,
|
||||
|
||||
onNavigateSettings = function()
|
||||
self:setState({
|
||||
appStatus = AppStatus.Settings,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
|
||||
Connecting = createPageElement(AppStatus.Connecting),
|
||||
|
||||
@@ -160,15 +206,7 @@ function App:render()
|
||||
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")
|
||||
self:endSession()
|
||||
end,
|
||||
}),
|
||||
|
||||
@@ -186,6 +224,7 @@ function App:render()
|
||||
onClose = function()
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
@@ -200,13 +239,54 @@ function App:render()
|
||||
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 = Assets.Images.PluginButton,
|
||||
icon = self.state.toolbarIcon,
|
||||
active = self.state.guiEnabled,
|
||||
enabled = true,
|
||||
onClick = function()
|
||||
@@ -223,4 +303,15 @@ function App:render()
|
||||
})
|
||||
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
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
15
rojo-test/build-tests/optional/default.project.json
Normal file
15
rojo-test/build-tests/optional/default.project.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
1
rojo-test/build-tests/optional/foo.txt
Normal file
1
rojo-test/build-tests/optional/foo.txt
Normal file
@@ -0,0 +1 @@
|
||||
Hello, from foo.txt!
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "root",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"folder": {
|
||||
"$path": "folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "child-projectname",
|
||||
"tree": {
|
||||
"$className": "Folder"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "root",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"folder": {
|
||||
"$path": "folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "child-projectname",
|
||||
"tree": {
|
||||
"$className": "Folder"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "root",
|
||||
"tree": {
|
||||
"$className": "Folder"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "weldconstraint",
|
||||
"tree": {
|
||||
"$path": "two-parts-welded.rbxmx"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "optional",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"create-later": {
|
||||
"$path": { "optional": "create-later" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
std = "roblox"
|
||||
std = "roblox+testez"
|
||||
|
||||
[config]
|
||||
unused_variable = { allow_unused_self = true }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
139
src/cli/sourcemap.rs
Normal file
139
src/cli/sourcemap.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
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);
|
||||
|
||||
let mut project_dir = project_path.to_path_buf();
|
||||
project_dir.pop();
|
||||
|
||||
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(), &project_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,
|
||||
})
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::Client::new();
|
||||
|
||||
log::debug!("Uploading to Roblox...");
|
||||
let mut 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(())
|
||||
}
|
||||
|
||||
146
src/project.rs
146
src/project.rs
@@ -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"}"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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\
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "weldconstraint",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
|
||||
"Workspace": {
|
||||
"Parts": {
|
||||
"$path": "two-parts-welded.rbxmx"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
testez.toml
Normal file
66
testez.toml
Normal 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 = []
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user