Compare commits

..

1 Commits

Author SHA1 Message Date
Lucien Greathouse
273974b74f SmallString implementation 2022-05-27 02:54:33 -04:00
67 changed files with 1320 additions and 4454 deletions

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
rust_version: [stable, 1.57.0] rust_version: [stable, 1.55.0]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@@ -67,7 +67,7 @@ jobs:
# -x86_64 to each release. # -x86_64 to each release.
include: include:
- host: linux - host: linux
os: ubuntu-18.04 os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
label: linux label: linux
@@ -150,4 +150,4 @@ jobs:
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
path: release.zip path: release.zip

3
.gitignore vendored
View File

@@ -19,3 +19,6 @@
# Snapshot files from the 'insta' Rust crate # Snapshot files from the 'insta' Rust crate
**/*.snap.new **/*.snap.new
# Selene generates a roblox.toml file that should not be checked in.
/roblox.toml

58
.luacheckrc Normal file
View File

@@ -0,0 +1,58 @@
stds.roblox = {
read_globals = {
game = {
other_fields = true,
},
-- Roblox globals
"script",
-- Extra functions
"tick", "warn", "spawn",
"wait", "settings", "typeof",
-- Types
"Vector2", "Vector3",
"Vector2int16", "Vector3int16",
"Color3",
"UDim", "UDim2",
"Rect",
"CFrame",
"Enum",
"Instance",
"DockWidgetPluginGuiInfo",
}
}
stds.plugin = {
read_globals = {
"plugin",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP", "itFIXME",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
"421", -- shadowing local variable
"422", -- shadowing argument
"431", -- shadowing upvalue
"432", -- shadowing upvalue argument
}
std = "lua51+roblox"
files["**/*.server.lua"] = {
std = "+plugin",
}
files["**/*.spec.lua"] = {
std = "+testez",
}

View File

@@ -1,29 +1,7 @@
# Rojo Changelog # Rojo Changelog
## Unreleased Changes ## Unreleased Changes
## [7.2.0] - June 29, 2022
* Added support for `.luau` files. ([#552])
* Added support for live syncing Attributes and Tags. ([#553])
* Added notification popups in the Roblox Studio plugin. ([#540])
* Fixed `init.meta.json` when used with `init.lua` and related files. ([#549])
* Fixed incorrect output when serving from a non-default address or port ([#556])
* Fixed Linux binaries not running on systems with older glibc. ([#561])
* Added `camelCase` casing for JSON models, deprecating `PascalCase` names. ([#563])
* Switched from structopt to clap for command line argument parsing. * Switched from structopt to clap for command line argument parsing.
* Significantly improved performance of building and serving. ([#548])
* Increased minimum supported Rust version to 1.57.0. ([#564])
[#540]: https://github.com/rojo-rbx/rojo/pull/540
[#548]: https://github.com/rojo-rbx/rojo/pull/548
[#549]: https://github.com/rojo-rbx/rojo/pull/549
[#552]: https://github.com/rojo-rbx/rojo/pull/552
[#553]: https://github.com/rojo-rbx/rojo/pull/553
[#556]: https://github.com/rojo-rbx/rojo/pull/556
[#561]: https://github.com/rojo-rbx/rojo/pull/561
[#563]: https://github.com/rojo-rbx/rojo/pull/563
[#564]: https://github.com/rojo-rbx/rojo/pull/564
[7.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.0
## [7.1.1] - May 26, 2022 ## [7.1.1] - May 26, 2022
* Fixed sourcemap command not stripping paths correctly ([#544]) * Fixed sourcemap command not stripping paths correctly ([#544])

559
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
[package] [package]
name = "rojo" name = "rojo-smallstring"
version = "7.2.0" version = "7.1.1"
rust-version = "1.57.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers" description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0" license = "MPL-2.0"
@@ -9,7 +8,7 @@ homepage = "https://rojo.space"
documentation = "https://rojo.space/docs" documentation = "https://rojo.space/docs"
repository = "https://github.com/rojo-rbx/rojo" repository = "https://github.com/rojo-rbx/rojo"
readme = "README.md" readme = "README.md"
edition = "2021" edition = "2018"
build = "build.rs" build = "build.rs"
exclude = [ exclude = [
@@ -28,8 +27,6 @@ default = []
# Enable this feature to live-reload assets from the web UI. # Enable this feature to live-reload assets from the web UI.
dev_live_assets = [] dev_live_assets = []
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
[workspace] [workspace]
members = ["crates/*"] members = ["crates/*"]
@@ -51,8 +48,8 @@ memofs = { version = "0.2.0", path = "crates/memofs" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" } # rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" } # rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.6.5" rbx_binary = "0.6.4"
rbx_dom_weak = "2.4.0" rbx_dom_weak = "2.3.0"
rbx_reflection = "4.2.0" rbx_reflection = "4.2.0"
rbx_reflection_database = "0.2.2" rbx_reflection_database = "0.2.2"
rbx_xml = "0.12.3" rbx_xml = "0.12.3"
@@ -83,8 +80,8 @@ thiserror = "1.0.30"
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] } tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
uuid = { version = "1.0.0", features = ["v4", "serde"] } uuid = { version = "1.0.0", features = ["v4", "serde"] }
clap = { version = "3.1.18", features = ["derive"] } clap = { version = "3.1.18", features = ["derive"] }
profiling = "1.0.6" smol_str = "0.1.23"
tracy-client = { version = "0.13.2", optional = true } profiling = { version = "1.0.6", features = ["profile-with-superluminal"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.10.1" winreg = "0.10.1"

View File

@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome! Pull requests are welcome!
Rojo supports Rust 1.57.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has. Rojo supports Rust 1.46.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
## License ## License
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details. Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.

Binary file not shown.

View File

@@ -21,7 +21,7 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
// We can skip any TestEZ test files since they aren't necessary for // We can skip any TestEZ test files since they aren't necessary for
// the plugin to run. // the plugin to run.
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") { if file_name.ends_with(".spec.lua") {
continue; continue;
} }

View File

@@ -1,4 +1,4 @@
[tools] [tools]
rojo = { source = "rojo-rbx/rojo", version = "7.1.1" } rojo = { source = "rojo-rbx/rojo", version = "7.1.1" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" } run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
selene = { source = "Kampfkarren/selene", version = "0.18.2" } selene = { source = "Kampfkarren/selene", version = "0.17.0" }

View File

@@ -23,45 +23,8 @@ end
local ALL_AXES = {"X", "Y", "Z"} local ALL_AXES = {"X", "Y", "Z"}
local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"} local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
local EncodedValue = {}
local types local types
types = { types = {
Attributes = {
fromPod = function(pod)
local output = {}
for key, value in pairs(pod) do
local ok, result = EncodedValue.decode(value)
if ok then
output[key] = result
else
local warning = ("Could not decode attribute value of type %q: %s"):format(typeof(value), tostring(result))
warn(warning)
end
end
return output
end,
toPod = function(roblox)
local output = {}
for key, value in pairs(roblox) do
local ok, result = EncodedValue.encodeNaive(value)
if ok then
output[key] = result
else
local warning = ("Could not encode attribute value of type %q: %s"):format(typeof(value), tostring(result))
warn(warning)
end
end
return output
end,
},
Axes = { Axes = {
fromPod = function(pod) fromPod = function(pod)
local axes = {} local axes = {}
@@ -470,6 +433,8 @@ types = {
}, },
} }
local EncodedValue = {}
function EncodedValue.decode(encodedValue) function EncodedValue.decode(encodedValue)
local ty, value = next(encodedValue) local ty, value = next(encodedValue)
@@ -494,19 +459,4 @@ function EncodedValue.encode(rbxValue, propertyType)
} }
end end
local propertyTypeRenames = {
number = "Float64",
boolean = "Bool",
string = "String",
}
function EncodedValue.encodeNaive(rbxValue)
local propertyType = typeof(rbxValue)
if propertyTypeRenames[propertyType] ~= nil then
propertyType = propertyTypeRenames[propertyType]
end
return EncodedValue.encode(rbxValue, propertyType)
end
return EncodedValue return EncodedValue

View File

@@ -1,73 +1,4 @@
{ {
"Attributes": {
"value": {
"Attributes": {
"TestBool": {
"Bool": true
},
"TestBrickColor": {
"BrickColor": 24
},
"TestColor3": {
"Color3": [
1.0,
0.5,
0.0
]
},
"TestNumber": {
"Float64": 1337.0
},
"TestRect": {
"Rect": [
[
1.0,
2.0
],
[
3.0,
4.0
]
]
},
"TestString": {
"String": "Test"
},
"TestUDim": {
"UDim": [
1.0,
2
]
},
"TestUDim2": {
"UDim2": [
[
1.0,
2
],
[
3.0,
4
]
]
},
"TestVector2": {
"Vector2": [
1.0,
2.0
]
},
"TestVector3": {
"Vector3": [
1.0,
2.0,
3.0
]
}
}
},
"ty": "Attributes"
},
"Axes": { "Axes": {
"value": { "value": {
"Axes": [ "Axes": [

View File

@@ -5,26 +5,6 @@ local CollectionService = game:GetService("CollectionService")
-- The reflection database refers to these as having scriptability = "Custom" -- The reflection database refers to these as having scriptability = "Custom"
return { return {
Instance = { Instance = {
Attributes = {
read = function(instance)
return true, instance:GetAttributes()
end,
write = function(instance, _, value)
local existing = instance:GetAttributes()
for key, attr in pairs(value) do
instance:SetAttribute(key, attr)
end
for key in pairs(existing) do
if value[key] == nil then
instance:SetAttribute(key, nil)
end
end
return true
end,
},
Tags = { Tags = {
read = function(instance) read = function(instance)
return true, CollectionService:GetTags(instance) return true, CollectionService:GetTags(instance)

File diff suppressed because it is too large Load Diff

View File

@@ -1,198 +0,0 @@
local TextService = game:GetService("TextService")
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Flipper = require(Rojo.Flipper)
local bindingUtil = require(script.Parent.bindingUtil)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local playSound = require(Plugin.playSound)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local baseClock = DateTime.now().UnixTimestampMillis
local e = Roact.createElement
local Notification = Roact.Component:extend("Notification")
function Notification:init()
self.motor = Flipper.SingleMotor.new(0)
self.binding = bindingUtil.fromMotor(self.motor)
self.lifetime = self.props.timeout
self.motor:onStep(function(value)
if value <= 0 then
if self.props.onClose then
self.props.onClose()
end
end
end)
end
function Notification:dismiss()
self.motor:setGoal(
Flipper.Spring.new(0, {
frequency = 5,
dampingRatio = 1,
})
)
end
function Notification:didMount()
self.motor:setGoal(
Flipper.Spring.new(1, {
frequency = 3,
dampingRatio = 1,
})
)
playSound(Assets.Sounds.Notification)
self.timeout = task.spawn(function()
local clock = os.clock()
local seen = false
while task.wait(1/10) do
local now = os.clock()
local dt = now - clock
clock = now
if not seen then
seen = StudioService.ActiveScript == nil
end
if not seen then
-- Don't run down timer before being viewed
continue
end
self.lifetime -= dt
if self.lifetime <= 0 then
self:dismiss()
break
end
end
end)
end
function Notification:willUnmount()
task.cancel(self.timeout)
end
function Notification:render()
local time = DateTime.fromUnixTimestampMillis(self.props.timestamp)
local textBounds = TextService:GetTextSize(
self.props.text,
15,
Enum.Font.GothamSemibold,
Vector2.new(350, 700)
)
local transparency = self.binding:map(function(value)
return 1 - value
end)
local size = self.binding:map(function(value)
return UDim2.fromOffset(
(35+40+textBounds.X)*value,
math.max(14+20+textBounds.Y, 32+20)
)
end)
return Theme.with(function(theme)
return e("TextButton", {
BackgroundTransparency = 1,
Size = size,
LayoutOrder = self.props.layoutOrder,
Text = "",
ClipsDescendants = true,
[Roact.Event.Activated] = function()
self:dismiss()
end,
}, {
e(BorderedContainer, {
transparency = transparency,
size = UDim2.new(1, 0, 1, 0),
}, {
TextContainer = e("Frame", {
Size = UDim2.new(0, 35+textBounds.X, 1, -20),
Position = UDim2.new(0, 0, 0, 10),
BackgroundTransparency = 1
}, {
Logo = e("ImageLabel", {
ImageTransparency = transparency,
Image = Assets.Images.PluginButton,
BackgroundTransparency = 1,
Size = UDim2.new(0, 32, 0, 32),
Position = UDim2.new(0, 0, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
}),
Info = e("TextLabel", {
Text = self.props.text,
Font = Enum.Font.GothamSemibold,
TextSize = 15,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
Position = UDim2.fromOffset(35, 0),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Time = e("TextLabel", {
Text = time:FormatLocalTime("LTS", "en-us"),
Font = Enum.Font.Code,
TextSize = 12,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, -35, 0, 14),
Position = UDim2.new(0, 35, 1, -14),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15),
}),
})
})
end)
end
local Notifications = Roact.Component:extend("Notifications")
function Notifications:render()
local notifs = {}
for index, notif in ipairs(self.props.notifications) do
notifs[notif] = e(Notification, {
text = notif.text,
timestamp = notif.timestamp,
timeout = notif.timeout,
layoutOrder = (notif.timestamp - baseClock),
onClose = function()
self.props.onClose(index)
end,
})
end
return Roact.createFragment(notifs)
end
return Notifications

View File

@@ -9,7 +9,6 @@ local Roact = require(Rojo.Roact)
local defaultSettings = { local defaultSettings = {
openScriptsExternally = false, openScriptsExternally = false,
twoWaySync = false, twoWaySync = false,
showNotifications = true,
} }
local Settings = {} local Settings = {}
@@ -119,4 +118,4 @@ end
return { return {
StudioProvider = StudioProvider, StudioProvider = StudioProvider,
with = with, with = with,
} }

View File

@@ -202,20 +202,12 @@ function SettingsPage:render()
layoutOrder = 1, layoutOrder = 1,
}), }),
ShowNotifications = e(Setting, {
id = "showNotifications",
name = "Show Notifications",
description = "Popup notifications in viewport",
transparency = self.props.transparency,
layoutOrder = 2,
}),
TwoWaySync = e(Setting, { TwoWaySync = e(Setting, {
id = "twoWaySync", id = "twoWaySync",
name = "Two-Way Sync", name = "Two-Way Sync",
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem", description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 3, layoutOrder = 2,
}), }),
Layout = e("UIListLayout", { Layout = e("UIListLayout", {
@@ -235,4 +227,4 @@ function SettingsPage:render()
end) end)
end end
return SettingsPage return SettingsPage

View File

@@ -103,10 +103,6 @@ local lightTheme = strict("LightTheme", {
LogoColor = BRAND_COLOR, LogoColor = BRAND_COLOR,
VersionColor = hexColor(0x727272), VersionColor = hexColor(0x727272),
}, },
Notification = {
InfoColor = hexColor(0x00000),
CloseColor = BRAND_COLOR,
},
ErrorColor = hexColor(0x000000), ErrorColor = hexColor(0x000000),
ScrollBarColor = hexColor(0x000000), ScrollBarColor = hexColor(0x000000),
}) })
@@ -181,10 +177,6 @@ local darkTheme = strict("DarkTheme", {
LogoColor = BRAND_COLOR, LogoColor = BRAND_COLOR,
VersionColor = hexColor(0xD3D3D3) VersionColor = hexColor(0xD3D3D3)
}, },
Notification = {
InfoColor = hexColor(0xFFFFFF),
CloseColor = hexColor(0xFFFFFF),
},
ErrorColor = hexColor(0xFFFFFF), ErrorColor = hexColor(0xFFFFFF),
ScrollBarColor = hexColor(0xFFFFFF), ScrollBarColor = hexColor(0xFFFFFF),
}) })

View File

@@ -16,7 +16,6 @@ local Theme = require(script.Theme)
local PluginSettings = require(script.PluginSettings) local PluginSettings = require(script.PluginSettings)
local Page = require(script.Page) local Page = require(script.Page)
local Notifications = require(script.Notifications)
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction) local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
local StudioToolbar = require(script.Components.Studio.StudioToolbar) local StudioToolbar = require(script.Components.Studio.StudioToolbar)
local StudioToggleButton = require(script.Components.Studio.StudioToggleButton) local StudioToggleButton = require(script.Components.Studio.StudioToggleButton)
@@ -45,37 +44,10 @@ function App:init()
self:setState({ self:setState({
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
guiEnabled = false, guiEnabled = false,
notifications = {},
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
}) })
end end
function App:addNotification(text: string, timeout: number?)
if not self.props.settings:get("showNotifications") then
return
end
local notifications = table.clone(self.state.notifications)
table.insert(notifications, {
text = text,
timestamp = DateTime.now().UnixTimestampMillis,
timeout = timeout or 3,
})
self:setState({
notifications = notifications,
})
end
function App:closeNotification(index: number)
local notifications = table.clone(self.state.notifications)
table.remove(notifications, index)
self:setState({
notifications = notifications,
})
end
function App:getHostAndPort() function App:getHostAndPort()
local host = self.host:getValue() local host = self.host:getValue()
local port = self.port:getValue() local port = self.port:getValue()
@@ -109,7 +81,6 @@ function App:startSession()
appStatus = AppStatus.Connecting, appStatus = AppStatus.Connecting,
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
}) })
self:addNotification("Connecting to session...")
elseif status == ServeSession.Status.Connected then elseif status == ServeSession.Status.Connected then
local address = ("%s:%s"):format(host, port) local address = ("%s:%s"):format(host, port)
self:setState({ self:setState({
@@ -118,7 +89,8 @@ function App:startSession()
address = address, address = address,
toolbarIcon = Assets.Images.PluginButtonConnected, toolbarIcon = Assets.Images.PluginButtonConnected,
}) })
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
Log.info("Connected to session '{}' at {}", details, address)
elseif status == ServeSession.Status.Disconnected then elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil self.serveSession = nil
@@ -132,13 +104,13 @@ function App:startSession()
errorMessage = tostring(details), errorMessage = tostring(details),
toolbarIcon = Assets.Images.PluginButtonWarning, toolbarIcon = Assets.Images.PluginButtonWarning,
}) })
self:addNotification(tostring(details), 10)
else else
self:setState({ self:setState({
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
}) })
self:addNotification("Disconnected from session.")
Log.info("Disconnected session")
end end
end end
end) end)
@@ -264,21 +236,6 @@ function App:render()
end), end),
}), }),
RojoNotifications = e("ScreenGui", {}, {
layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
Padding = UDim.new(0, 5),
}),
notifs = e(Notifications, {
notifications = self.state.notifications,
onClose = function(index)
self:closeNotification(index)
end,
}),
}),
toggleAction = e(StudioPluginAction, { toggleAction = e(StudioPluginAction, {
name = "RojoConnection", name = "RojoConnection",
title = "Rojo: Connect/Disconnect", title = "Rojo: Connect/Disconnect",

View File

@@ -45,9 +45,6 @@ local Assets = {
[500] = "rbxassetid://2609138523" [500] = "rbxassetid://2609138523"
}, },
}, },
Sounds = {
Notification = "rbxassetid://9716079936",
},
StartSession = "", StartSession = "",
SessionActive = "", SessionActive = "",
Configure = "", Configure = "",
@@ -65,4 +62,4 @@ end
guardForTypos("Assets", Assets) guardForTypos("Assets", Assets)
return Assets return Assets

View File

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

View File

@@ -18,7 +18,7 @@ local App = require(script.App)
local app = Roact.createElement(App, { local app = Roact.createElement(App, {
plugin = plugin plugin = plugin
}) })
local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI") local tree = Roact.mount(app, nil, "Rojo UI")
plugin.Unloading:Connect(function() plugin.Unloading:Connect(function()
Roact.unmount(tree) Roact.unmount(tree)
@@ -28,4 +28,4 @@ if Config.isDevBuild then
local TestEZ = require(script.Parent.TestEZ) local TestEZ = require(script.Parent.TestEZ)
require(script.runTests)(TestEZ) require(script.runTests)(TestEZ)
end end

View File

@@ -1,22 +0,0 @@
-- Roblox decided that sounds only play in Edit mode when parented to a plugin widget, for some reason
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
local widget = plugin:CreateDockWidgetPluginGui("Rojo_soundPlayer", DockWidgetPluginGuiInfo.new(
Enum.InitialDockState.Float,
false, true,
10, 10,
10, 10
))
widget.Name = "Rojo_soundPlayer"
widget.Title = "Rojo Sound Player"
return function(soundId)
local sound = Instance.new("Sound")
sound.SoundId = soundId
sound.Parent = widget
sound.Ended:Connect(function()
sound:Destroy()
end)
sound:Play()
end

View File

@@ -1,24 +0,0 @@
---
source: tests/tests/build.rs
assertion_line: 99
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">attributes</string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">Explicit</string>
<BinaryString name="AttributesSerialize">AgAAAAUAAABIZWxsbwIFAAAAV29ybGQGAAAAVmVjdG9yEQAAgD8AAABAAABAQA==</BinaryString>
</Properties>
</Item>
<Item class="Folder" referent="2">
<Properties>
<string name="Name">ImplicitAttributes</string>
<BinaryString name="AttributesSerialize">AgAAAAMAAABIZXkCBwAAAEdyYW5kbWEGAAAAVmVjdG9yEQAAgEAAAKBAAADAQA==</BinaryString>
</Properties>
</Item>
</Item>
</roblox>

View File

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

View File

@@ -1,13 +1,14 @@
--- ---
source: tests/tests/build.rs source: tests/tests/build.rs
assertion_line: 99
expression: contents expression: contents
--- ---
<roblox version="4"> <roblox version="4">
<Item class="Folder" referent="0"> <Item class="Folder" referent="0">
<Properties> <Properties>
<string name="Name">weldconstraint</string> <string name="Name">weldconstraint</string>
<BinaryString name="AttributesSerialize"></BinaryString> <BinaryString name="AttributesSerialize">
</BinaryString>
<int64 name="SourceAssetId">-1</int64> <int64 name="SourceAssetId">-1</int64>
<BinaryString name="Tags"></BinaryString> <BinaryString name="Tags"></BinaryString>
</Properties> </Properties>
@@ -15,7 +16,8 @@ expression: contents
<Properties> <Properties>
<string name="Name">A</string> <string name="Name">A</string>
<bool name="Anchored">false</bool> <bool name="Anchored">false</bool>
<BinaryString name="AttributesSerialize"></BinaryString> <BinaryString name="AttributesSerialize">
</BinaryString>
<float name="BackParamA">-0.5</float> <float name="BackParamA">-0.5</float>
<float name="BackParamB">0.5</float> <float name="BackParamB">0.5</float>
<token name="BackSurface">0</token> <token name="BackSurface">0</token>
@@ -106,7 +108,8 @@ expression: contents
<Item class="WeldConstraint" referent="2"> <Item class="WeldConstraint" referent="2">
<Properties> <Properties>
<string name="Name">WeldConstraint</string> <string name="Name">WeldConstraint</string>
<BinaryString name="AttributesSerialize"></BinaryString> <BinaryString name="AttributesSerialize">
</BinaryString>
<CoordinateFrame name="CFrame0"> <CoordinateFrame name="CFrame0">
<X>7</X> <X>7</X>
<Y>0.000001013279</Y> <Y>0.000001013279</Y>
@@ -133,7 +136,8 @@ expression: contents
<Properties> <Properties>
<string name="Name">B</string> <string name="Name">B</string>
<bool name="Anchored">false</bool> <bool name="Anchored">false</bool>
<BinaryString name="AttributesSerialize"></BinaryString> <BinaryString name="AttributesSerialize">
</BinaryString>
<float name="BackParamA">-0.5</float> <float name="BackParamA">-0.5</float>
<float name="BackParamB">0.5</float> <float name="BackParamB">0.5</float>
<token name="BackSurface">0</token> <token name="BackSurface">0</token>

View File

@@ -1,36 +0,0 @@
{
"name": "attributes",
"tree": {
"$className": "Folder",
"Explicit": {
"$className": "Folder",
"$properties": {
"Attributes": {
"Attributes": {
"Hello": {
"String": "World"
},
"Vector": {
"Vector3": [1, 2, 3]
}
}
}
}
},
"ImplicitAttributes": {
"$className": "Folder",
"$properties": {
"Attributes": {
"Hey": {
"String": "Grandma"
},
"Vector": {
"Vector3": [4, 5, 6]
}
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,6 +46,7 @@ pub struct ChangeProcessor {
impl ChangeProcessor { impl ChangeProcessor {
/// Spin up the ChangeProcessor, connecting it to the given tree, VFS, and /// Spin up the ChangeProcessor, connecting it to the given tree, VFS, and
/// outbound message queue. /// outbound message queue.
#[profiling::function]
pub fn start( pub fn start(
tree: Arc<Mutex<RojoTree>>, tree: Arc<Mutex<RojoTree>>,
vfs: Arc<Vfs>, vfs: Arc<Vfs>,
@@ -291,7 +292,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
} }
}; };
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) apply_patch_set(tree, patch_set)
} }
Ok(None) => { Ok(None) => {
@@ -334,7 +335,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
} }
}; };
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) apply_patch_set(tree, patch_set)
} }
}; };

View File

@@ -1,6 +1,5 @@
use std::{ use std::{
io::{BufWriter, Write}, io::{BufWriter, Write},
mem::forget,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@@ -62,10 +61,6 @@ impl BuildCommand {
} }
} }
// Avoid dropping ServeSession: it's potentially VERY expensive to drop
// and we're about to exit anyways.
forget(session);
Ok(()) Ok(())
} }
} }

View File

@@ -67,17 +67,15 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
let writer = BufferWriter::stdout(color); let writer = BufferWriter::stdout(color);
let mut buffer = writer.buffer(); let mut buffer = writer.buffer();
let address_string = if bind_address.is_loopback() {
"localhost".to_owned()
} else {
bind_address.to_string()
};
writeln!(&mut buffer, "Rojo server listening:")?; writeln!(&mut buffer, "Rojo server listening:")?;
write!(&mut buffer, " Address: ")?; write!(&mut buffer, " Address: ")?;
buffer.set_color(&green)?; buffer.set_color(&green)?;
writeln!(&mut buffer, "{}", address_string)?; if bind_address.is_loopback() {
writeln!(&mut buffer, "localhost")?;
} else {
writeln!(&mut buffer, "{}", bind_address)?;
}
buffer.set_color(&ColorSpec::new())?; buffer.set_color(&ColorSpec::new())?;
write!(&mut buffer, " Port: ")?; write!(&mut buffer, " Port: ")?;
@@ -90,7 +88,7 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
write!(&mut buffer, "Visit ")?; write!(&mut buffer, "Visit ")?;
buffer.set_color(&green)?; buffer.set_color(&green)?;
write!(&mut buffer, "http://{}:{}/", address_string, port)?; write!(&mut buffer, "http://localhost:{}/", port)?;
buffer.set_color(&ColorSpec::new())?; buffer.set_color(&ColorSpec::new())?;
writeln!(&mut buffer, " in your browser for more information.")?; writeln!(&mut buffer, " in your browser for more information.")?;

View File

@@ -18,6 +18,7 @@ mod project;
mod resolution; mod resolution;
mod serve_session; mod serve_session;
mod session_id; mod session_id;
mod small_string;
mod snapshot; mod snapshot;
mod snapshot_middleware; mod snapshot_middleware;
mod web; mod web;

View File

@@ -6,8 +6,7 @@ use clap::Parser;
use librojo::cli::Options; use librojo::cli::Options;
fn main() { fn main() {
#[cfg(feature = "profile-with-tracy")] profiling::register_thread!("Main Thread");
tracy_client::Client::start();
panic::set_hook(Box::new(|panic_info| { panic::set_hook(Box::new(|panic_info| {
// PanicInfo's payload is usually a &'static str or String. // PanicInfo's payload is usually a &'static str or String.

View File

@@ -2,8 +2,7 @@ use std::borrow::Borrow;
use anyhow::format_err; use anyhow::format_err;
use rbx_dom_weak::types::{ use rbx_dom_weak::types::{
Attributes, CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3,
Vector3,
}; };
use rbx_reflection::{DataType, PropertyDescriptor}; use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -41,7 +40,6 @@ pub enum AmbiguousValue {
Array3([f64; 3]), Array3([f64; 3]),
Array4([f64; 4]), Array4([f64; 4]),
Array12([f64; 12]), Array12([f64; 12]),
Attributes(Attributes),
} }
impl AmbiguousValue { impl AmbiguousValue {
@@ -130,8 +128,6 @@ impl AmbiguousValue {
Ok(CFrame::new(pos, orientation).into()) Ok(CFrame::new(pos, orientation).into())
} }
(VariantType::Attributes, AmbiguousValue::Attributes(value)) => Ok(value.into()),
(_, unresolved) => Err(format_err!( (_, unresolved) => Err(format_err!(
"Wrong type of value for property {}.{}. Expected {:?}, got {}", "Wrong type of value for property {}.{}. Expected {:?}, got {}",
class_name, class_name,
@@ -158,7 +154,6 @@ impl AmbiguousValue {
AmbiguousValue::Array3(_) => "an array of three numbers", AmbiguousValue::Array3(_) => "an array of three numbers",
AmbiguousValue::Array4(_) => "an array of four numbers", AmbiguousValue::Array4(_) => "an array of four numbers",
AmbiguousValue::Array12(_) => "an array of twelve numbers", AmbiguousValue::Array12(_) => "an array of twelve numbers",
AmbiguousValue::Attributes(_) => "an object containing attributes",
} }
} }
} }

View File

@@ -96,6 +96,7 @@ impl ServeSession {
/// The project file is expected to be loaded out-of-band since it's /// The project file is expected to be loaded out-of-band since it's
/// currently loaded from the filesystem directly instead of through the /// currently loaded from the filesystem directly instead of through the
/// in-memory filesystem layer. /// in-memory filesystem layer.
#[profiling::function]
pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Result<Self, ServeSessionError> { pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Result<Self, ServeSessionError> {
let start_path = start_path.as_ref(); let start_path = start_path.as_ref();
let start_time = Instant::now(); let start_time = Instant::now();
@@ -130,7 +131,7 @@ impl ServeSession {
let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?; let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?;
log::trace!("Computing initial patch set"); 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"); log::trace!("Applying initial patch set");
apply_patch_set(&mut tree, patch_set); apply_patch_set(&mut tree, patch_set);

1
src/small_string.rs Normal file
View File

@@ -0,0 +1 @@
pub use smol_str::SmolStr as SmallString;

View File

@@ -1,13 +1,15 @@
//! Defines the structure of an instance snapshot. //! Defines the structure of an instance snapshot.
use std::{borrow::Cow, collections::HashMap}; use std::collections::HashMap;
use rbx_dom_weak::{ use rbx_dom_weak::{
types::{Ref, Variant}, types::{Ref, Variant},
Instance, WeakDom, WeakDom,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::small_string::SmallString;
use super::InstanceMetadata; use super::InstanceMetadata;
/// A lightweight description of what an instance should look like. /// A lightweight description of what an instance should look like.
@@ -25,13 +27,13 @@ pub struct InstanceSnapshot {
pub metadata: InstanceMetadata, pub metadata: InstanceMetadata,
/// Correpsonds to the Name property of the instance. /// Correpsonds to the Name property of the instance.
pub name: Cow<'static, str>, pub name: SmallString,
/// Corresponds to the ClassName property of the instance. /// Corresponds to the ClassName property of the instance.
pub class_name: Cow<'static, str>, pub class_name: SmallString,
/// All other properties of the instance, weakly-typed. /// All other properties of the instance, weakly-typed.
pub properties: HashMap<String, Variant>, pub properties: HashMap<SmallString, Variant>,
/// The children of the instance represented as more snapshots. /// The children of the instance represented as more snapshots.
/// ///
@@ -44,37 +46,37 @@ impl InstanceSnapshot {
Self { Self {
snapshot_id: None, snapshot_id: None,
metadata: InstanceMetadata::default(), metadata: InstanceMetadata::default(),
name: Cow::Borrowed("DEFAULT"), name: "DEFAULT".into(),
class_name: Cow::Borrowed("DEFAULT"), class_name: "DEFAULT".into(),
properties: HashMap::new(), properties: HashMap::new(),
children: Vec::new(), children: Vec::new(),
} }
} }
pub fn name(self, name: impl Into<String>) -> Self { pub fn name(self, name: impl Into<SmallString>) -> Self {
Self { Self {
name: Cow::Owned(name.into()), name: name.into(),
..self ..self
} }
} }
pub fn class_name(self, class_name: impl Into<String>) -> Self { pub fn class_name(self, class_name: impl Into<SmallString>) -> Self {
Self { Self {
class_name: Cow::Owned(class_name.into()), class_name: class_name.into(),
..self ..self
} }
} }
pub fn property<K, V>(mut self, key: K, value: V) -> Self pub fn property<K, V>(mut self, key: K, value: V) -> Self
where where
K: Into<String>, K: Into<SmallString>,
V: Into<Variant>, V: Into<Variant>,
{ {
self.properties.insert(key.into(), value.into()); self.properties.insert(key.into(), value.into());
self self
} }
pub fn properties(self, properties: impl Into<HashMap<String, Variant>>) -> Self { pub fn properties(self, properties: impl Into<HashMap<SmallString, Variant>>) -> Self {
Self { Self {
properties: properties.into(), properties: properties.into(),
..self ..self
@@ -102,29 +104,28 @@ impl InstanceSnapshot {
} }
} }
#[profiling::function] pub fn from_tree(tree: &WeakDom, id: Ref) -> Self {
pub fn from_tree(tree: WeakDom, id: Ref) -> Self { let instance = tree.get_by_ref(id).expect("instance did not exist in tree");
let (_, mut raw_tree) = tree.into_raw();
Self::from_raw_tree(&mut raw_tree, id)
}
fn from_raw_tree(raw_tree: &mut HashMap<Ref, Instance>, id: Ref) -> Self {
let instance = raw_tree
.remove(&id)
.expect("instance did not exist in tree");
let children = instance let children = instance
.children() .children()
.iter() .iter()
.map(|&id| Self::from_raw_tree(raw_tree, id)) .copied()
.map(|id| Self::from_tree(tree, id))
.collect();
let properties = instance
.properties
.iter()
.map(|(key, value)| (key.into(), value.clone()))
.collect(); .collect();
Self { Self {
snapshot_id: Some(id), snapshot_id: Some(id),
metadata: InstanceMetadata::default(), metadata: InstanceMetadata::default(),
name: Cow::Owned(instance.name), name: SmallString::from(&instance.name),
class_name: Cow::Owned(instance.class), class_name: SmallString::from(&instance.class),
properties: instance.properties, properties,
children, children,
} }
} }

View File

@@ -5,6 +5,8 @@ use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant}; use rbx_dom_weak::types::{Ref, Variant};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::small_string::SmallString;
use super::{InstanceMetadata, InstanceSnapshot}; use super::{InstanceMetadata, InstanceSnapshot};
/// A set of different kinds of patches that can be applied to an WeakDom. /// A set of different kinds of patches that can be applied to an WeakDom.
@@ -40,12 +42,12 @@ pub struct PatchAdd {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchUpdate { pub struct PatchUpdate {
pub id: Ref, pub id: Ref,
pub changed_name: Option<String>, pub changed_name: Option<SmallString>,
pub changed_class_name: Option<String>, pub changed_class_name: Option<SmallString>,
/// Contains all changed properties. If a property is assigned to `None`, /// Contains all changed properties. If a property is assigned to `None`,
/// then that property has been removed. /// then that property has been removed.
pub changed_properties: HashMap<String, Option<Variant>>, pub changed_properties: HashMap<SmallString, Option<Variant>>,
/// Changed Rojo-specific metadata, if any of it changed. /// Changed Rojo-specific metadata, if any of it changed.
pub changed_metadata: Option<InstanceMetadata>, pub changed_metadata: Option<InstanceMetadata>,
@@ -83,9 +85,9 @@ pub struct AppliedPatchUpdate {
pub id: Ref, pub id: Ref,
// TODO: Store previous values in order to detect application conflicts // TODO: Store previous values in order to detect application conflicts
pub changed_name: Option<String>, pub changed_name: Option<SmallString>,
pub changed_class_name: Option<String>, pub changed_class_name: Option<SmallString>,
pub changed_properties: HashMap<String, Option<Variant>>, pub changed_properties: HashMap<SmallString, Option<Variant>>,
pub changed_metadata: Option<InstanceMetadata>, pub changed_metadata: Option<InstanceMetadata>,
} }

View File

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

View File

@@ -1,10 +1,7 @@
//! Defines the algorithm for computing a roughly-minimal patch set given an //! Defines the algorithm for computing a roughly-minimal patch set given an
//! existing instance tree and an instance snapshot. //! existing instance tree and an instance snapshot.
use std::{ use std::collections::{HashMap, HashSet};
collections::{HashMap, HashSet},
mem::take,
};
use rbx_dom_weak::types::{Ref, Variant}; use rbx_dom_weak::types::{Ref, Variant};
@@ -14,7 +11,11 @@ use super::{
}; };
#[profiling::function] #[profiling::function]
pub fn compute_patch_set(snapshot: Option<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 patch_set = PatchSet::new();
if let Some(snapshot) = snapshot { if let Some(snapshot) = snapshot {
@@ -74,7 +75,7 @@ fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut Instan
fn compute_patch_set_internal( fn compute_patch_set_internal(
context: &mut ComputePatchContext, context: &mut ComputePatchContext,
mut snapshot: InstanceSnapshot, snapshot: &InstanceSnapshot,
tree: &RojoTree, tree: &RojoTree,
id: Ref, id: Ref,
patch_set: &mut PatchSet, patch_set: &mut PatchSet,
@@ -87,12 +88,12 @@ fn compute_patch_set_internal(
.get_instance(id) .get_instance(id)
.expect("Instance did not exist in tree"); .expect("Instance did not exist in tree");
compute_property_patches(&mut snapshot, &instance, patch_set); compute_property_patches(snapshot, &instance, patch_set);
compute_children_patches(context, &mut snapshot, tree, id, patch_set); compute_children_patches(context, snapshot, tree, id, patch_set);
} }
fn compute_property_patches( fn compute_property_patches(
snapshot: &mut InstanceSnapshot, snapshot: &InstanceSnapshot,
instance: &InstanceWithMeta, instance: &InstanceWithMeta,
patch_set: &mut PatchSet, patch_set: &mut PatchSet,
) { ) {
@@ -102,32 +103,32 @@ fn compute_property_patches(
let changed_name = if snapshot.name == instance.name() { let changed_name = if snapshot.name == instance.name() {
None None
} else { } else {
Some(take(&mut snapshot.name).into_owned()) Some(snapshot.name.clone())
}; };
let changed_class_name = if snapshot.class_name == instance.class_name() { let changed_class_name = if snapshot.class_name == instance.class_name() {
None None
} else { } else {
Some(take(&mut snapshot.class_name).into_owned()) Some(snapshot.class_name.clone())
}; };
let changed_metadata = if &snapshot.metadata == instance.metadata() { let changed_metadata = if &snapshot.metadata == instance.metadata() {
None None
} else { } else {
Some(take(&mut snapshot.metadata)) Some(snapshot.metadata.clone())
}; };
for (name, snapshot_value) in take(&mut snapshot.properties) { for (name, snapshot_value) in &snapshot.properties {
visited_properties.insert(name.clone()); visited_properties.insert(name.as_str());
match instance.properties().get(&name) { match instance.properties().get(name.as_str()) {
Some(instance_value) => { Some(instance_value) => {
if &snapshot_value != instance_value { if snapshot_value != instance_value {
changed_properties.insert(name, Some(snapshot_value)); changed_properties.insert(name.clone(), Some(snapshot_value.clone()));
} }
} }
None => { None => {
changed_properties.insert(name, Some(snapshot_value)); changed_properties.insert(name.clone(), Some(snapshot_value.clone()));
} }
} }
} }
@@ -137,7 +138,7 @@ fn compute_property_patches(
continue; continue;
} }
changed_properties.insert(name.clone(), None); changed_properties.insert(name.into(), None);
} }
if changed_properties.is_empty() if changed_properties.is_empty()
@@ -159,7 +160,7 @@ fn compute_property_patches(
fn compute_children_patches( fn compute_children_patches(
context: &mut ComputePatchContext, context: &mut ComputePatchContext,
snapshot: &mut InstanceSnapshot, snapshot: &InstanceSnapshot,
tree: &RojoTree, tree: &RojoTree,
id: Ref, id: Ref,
patch_set: &mut PatchSet, patch_set: &mut PatchSet,
@@ -172,7 +173,7 @@ fn compute_children_patches(
let mut paired_instances = vec![false; instance_children.len()]; let mut paired_instances = vec![false; instance_children.len()];
for snapshot_child in take(&mut snapshot.children) { for snapshot_child in snapshot.children.iter() {
let matching_instance = let matching_instance =
instance_children instance_children
.iter() .iter()
@@ -209,7 +210,7 @@ fn compute_children_patches(
None => { None => {
patch_set.added_instances.push(PatchAdd { patch_set.added_instances.push(PatchAdd {
parent_id: id, parent_id: id,
instance: snapshot_child, instance: snapshot_child.clone(),
}); });
} }
} }
@@ -257,7 +258,7 @@ mod test {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(Some(snapshot), &tree, root_id); let patch_set = compute_patch_set(Some(&snapshot), &tree, root_id);
let expected_patch_set = PatchSet { let expected_patch_set = PatchSet {
updated_instances: vec![PatchUpdate { updated_instances: vec![PatchUpdate {
@@ -307,7 +308,7 @@ mod test {
class_name: Cow::Borrowed("foo"), class_name: Cow::Borrowed("foo"),
}; };
let patch_set = compute_patch_set(Some(snapshot), &tree, root_id); let patch_set = compute_patch_set(Some(&snapshot), &tree, root_id);
let expected_patch_set = PatchSet { let expected_patch_set = PatchSet {
added_instances: vec![PatchAdd { added_instances: vec![PatchAdd {

View File

@@ -23,7 +23,7 @@ fn set_name_and_class_name() {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(Some(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); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);
@@ -47,7 +47,7 @@ fn set_property() {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(Some(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); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);
@@ -78,7 +78,7 @@ fn remove_property() {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(Some(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); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);
@@ -107,7 +107,7 @@ fn add_child() {
}], }],
}; };
let patch_set = compute_patch_set(Some(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); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);
@@ -139,7 +139,7 @@ fn remove_child() {
children: Vec::new(), children: Vec::new(),
}; };
let patch_set = compute_patch_set(Some(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); let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value); assert_yaml_snapshot!(patch_value);

View File

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

View File

@@ -30,7 +30,7 @@ pub fn snapshot_csv(
.name(name) .name(name)
.class_name("LocalizationTable") .class_name("LocalizationTable")
.properties(hashmap! { .properties(hashmap! {
"Contents".to_owned() => table_contents.into(), "Contents".into() => table_contents.into(),
}) })
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()

View File

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

View File

@@ -25,7 +25,7 @@ pub fn snapshot_json(
let as_lua = json_to_lua(value).to_string(); let as_lua = json_to_lua(value).to_string();
let properties = hashmap! { let properties = hashmap! {
"Source".to_owned() => as_lua.into(), "Source".into() => as_lua.into(),
}; };
let meta_path = path.with_file_name(format!("{}.meta.json", name)); let meta_path = path.with_file_name(format!("{}.meta.json", name));

View File

@@ -1,4 +1,4 @@
use std::{borrow::Cow, collections::HashMap, path::Path, str}; use std::{collections::HashMap, path::Path, str};
use anyhow::Context; use anyhow::Context;
use memofs::Vfs; use memofs::Vfs;
@@ -26,25 +26,12 @@ pub fn snapshot_json_model(
return Ok(None); return Ok(None);
} }
let mut instance: JsonModel = serde_json::from_str(contents_str) let instance: JsonModel = serde_json::from_str(contents_str)
.with_context(|| format!("File is not a valid JSON model: {}", path.display()))?; .with_context(|| format!("File is not a valid JSON model: {}", path.display()))?;
if let Some(top_level_name) = &instance.name {
let new_name = format!("{}.model.json", top_level_name);
log::warn!(
"Model at path {} had a top-level Name field. \
This field has been ignored since Rojo 6.0.\n\
Consider removing this field and renaming the file to {}.",
new_name,
path.display()
);
}
instance.name = Some(name.to_owned());
let mut snapshot = instance let mut snapshot = instance
.into_snapshot() .core
.into_snapshot(name.to_owned())
.with_context(|| format!("Could not load JSON model: {}", path.display()))?; .with_context(|| format!("Could not load JSON model: {}", path.display()))?;
snapshot.metadata = snapshot snapshot.metadata = snapshot
@@ -57,50 +44,55 @@ pub fn snapshot_json_model(
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "PascalCase")]
struct JsonModel { struct JsonModel {
#[serde(alias = "Name")]
name: Option<String>, name: Option<String>,
#[serde(alias = "ClassName")] #[serde(flatten)]
core: JsonModelCore,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModelInstance {
name: String,
#[serde(flatten)]
core: JsonModelCore,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModelCore {
class_name: String, class_name: String,
#[serde( #[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")]
alias = "Children", children: Vec<JsonModelInstance>,
default = "Vec::new",
skip_serializing_if = "Vec::is_empty"
)]
children: Vec<JsonModel>,
#[serde( #[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
alias = "Properties",
default = "HashMap::new",
skip_serializing_if = "HashMap::is_empty"
)]
properties: HashMap<String, UnresolvedValue>, properties: HashMap<String, UnresolvedValue>,
} }
impl JsonModel { impl JsonModelCore {
fn into_snapshot(self) -> anyhow::Result<InstanceSnapshot> { fn into_snapshot(self, name: String) -> anyhow::Result<InstanceSnapshot> {
let name = self.name.unwrap_or_else(|| self.class_name.clone());
let class_name = self.class_name; let class_name = self.class_name;
let mut children = Vec::with_capacity(self.children.len()); let mut children = Vec::with_capacity(self.children.len());
for child in self.children { for child in self.children {
children.push(child.into_snapshot()?); children.push(child.core.into_snapshot(child.name)?);
} }
let mut properties = HashMap::with_capacity(self.properties.len()); let mut properties = HashMap::with_capacity(self.properties.len());
for (key, unresolved) in self.properties { for (key, unresolved) in self.properties {
let value = unresolved.resolve(&class_name, &key)?; let value = unresolved.resolve(&class_name, &key)?;
properties.insert(key, value); properties.insert(key.into(), value);
} }
Ok(InstanceSnapshot { Ok(InstanceSnapshot {
snapshot_id: None, snapshot_id: None,
metadata: Default::default(), metadata: Default::default(),
name: Cow::Owned(name), name: name.into(),
class_name: Cow::Owned(class_name), class_name: class_name.into(),
properties, properties,
children, children,
}) })
@@ -121,43 +113,7 @@ mod test {
VfsSnapshot::file( VfsSnapshot::file(
r#" r#"
{ {
"className": "IntValue", "Name": "children",
"properties": {
"Value": 5
},
"children": [
{
"name": "The Child",
"className": "StringValue"
}
]
}
"#,
),
)
.unwrap();
let vfs = Vfs::new(imfs);
let instance_snapshot = snapshot_json_model(
&InstanceContext::default(),
&vfs,
Path::new("/foo.model.json"),
)
.unwrap()
.unwrap();
insta::assert_yaml_snapshot!(instance_snapshot);
}
#[test]
fn model_from_vfs_legacy() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo.model.json",
VfsSnapshot::file(
r#"
{
"ClassName": "IntValue", "ClassName": "IntValue",
"Properties": { "Properties": {
"Value": 5 "Value": 5
@@ -174,11 +130,11 @@ mod test {
) )
.unwrap(); .unwrap();
let vfs = Vfs::new(imfs); let mut vfs = Vfs::new(imfs);
let instance_snapshot = snapshot_json_model( let instance_snapshot = snapshot_json_model(
&InstanceContext::default(), &InstanceContext::default(),
&vfs, &mut vfs,
Path::new("/foo.model.json"), Path::new("/foo.model.json"),
) )
.unwrap() .unwrap()

View File

@@ -6,11 +6,7 @@ use memofs::{IoResultExt, Vfs};
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::{ use super::{dir::snapshot_dir, meta_file::AdjacentMetadata, util::match_trailing};
dir::{dir_meta, snapshot_dir_no_meta},
meta_file::AdjacentMetadata,
util::match_trailing,
};
/// Core routine for turning Lua files into snapshots. /// Core routine for turning Lua files into snapshots.
pub fn snapshot_lua( pub fn snapshot_lua(
@@ -27,12 +23,6 @@ pub fn snapshot_lua(
("LocalScript", name) ("LocalScript", name)
} else if let Some(name) = match_trailing(&file_name, ".lua") { } else if let Some(name) = match_trailing(&file_name, ".lua") {
("ModuleScript", name) ("ModuleScript", name)
} else if let Some(name) = match_trailing(&file_name, ".server.luau") {
("Script", name)
} else if let Some(name) = match_trailing(&file_name, ".client.luau") {
("LocalScript", name)
} else if let Some(name) = match_trailing(&file_name, ".luau") {
("ModuleScript", name)
} else { } else {
return Ok(None); return Ok(None);
}; };
@@ -48,7 +38,7 @@ pub fn snapshot_lua(
.name(instance_name) .name(instance_name)
.class_name(class_name) .class_name(class_name)
.properties(hashmap! { .properties(hashmap! {
"Source".to_owned() => contents_str.into(), "Source".into() => contents_str.into(),
}) })
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()
@@ -76,7 +66,7 @@ pub fn snapshot_lua_init(
init_path: &Path, init_path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> { ) -> anyhow::Result<Option<InstanceSnapshot>> {
let folder_path = init_path.parent().unwrap(); let folder_path = init_path.parent().unwrap();
let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap(); let dir_snapshot = snapshot_dir(context, vfs, folder_path)?.unwrap();
if dir_snapshot.class_name != "Folder" { if dir_snapshot.class_name != "Folder" {
anyhow::bail!( anyhow::bail!(
@@ -96,10 +86,6 @@ pub fn snapshot_lua_init(
init_snapshot.children = dir_snapshot.children; init_snapshot.children = dir_snapshot.children;
init_snapshot.metadata = dir_snapshot.metadata; init_snapshot.metadata = dir_snapshot.metadata;
if let Some(mut meta) = dir_meta(vfs, folder_path)? {
meta.apply_all(&mut init_snapshot)?;
}
Ok(Some(init_snapshot)) Ok(Some(init_snapshot))
} }

View File

@@ -1,4 +1,4 @@
use std::{borrow::Cow, collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use anyhow::{format_err, Context}; use anyhow::{format_err, Context};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -49,7 +49,7 @@ impl AdjacentMetadata {
.resolve(&snapshot.class_name, &key) .resolve(&snapshot.class_name, &key)
.with_context(|| format!("error applying meta file {}", path.display()))?; .with_context(|| format!("error applying meta file {}", path.display()))?;
snapshot.properties.insert(key, value); snapshot.properties.insert(key.into(), value);
} }
Ok(()) Ok(())
@@ -116,7 +116,7 @@ impl DirectoryMetadata {
)); ));
} }
snapshot.class_name = Cow::Owned(class_name); snapshot.class_name = class_name.into();
} }
Ok(()) Ok(())
@@ -136,7 +136,7 @@ impl DirectoryMetadata {
.resolve(&snapshot.class_name, &key) .resolve(&snapshot.class_name, &key)
.with_context(|| format!("error applying meta file {}", path.display()))?; .with_context(|| format!("error applying meta file {}", path.display()))?;
snapshot.properties.insert(key, value); snapshot.properties.insert(key.into(), value);
} }
Ok(()) Ok(())

View File

@@ -57,31 +57,16 @@ pub fn snapshot_from_vfs(
return snapshot_project(context, vfs, &project_path); return snapshot_project(context, vfs, &project_path);
} }
let init_path = path.join("init.luau");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.lua"); let init_path = path.join("init.lua");
if vfs.metadata(&init_path).with_not_found()?.is_some() { if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path); return snapshot_lua_init(context, vfs, &init_path);
} }
let init_path = path.join("init.server.luau");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.server.lua"); let init_path = path.join("init.server.lua");
if vfs.metadata(&init_path).with_not_found()?.is_some() { if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path); return snapshot_lua_init(context, vfs, &init_path);
} }
let init_path = path.join("init.client.luau");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.client.lua"); let init_path = path.join("init.client.lua");
if vfs.metadata(&init_path).with_not_found()?.is_some() { if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path); return snapshot_lua_init(context, vfs, &init_path);
@@ -89,11 +74,7 @@ pub fn snapshot_from_vfs(
snapshot_dir(context, vfs, path) snapshot_dir(context, vfs, path)
} else { } else {
let script_name = path if let Ok(name) = path.file_name_trim_end(".lua") {
.file_name_trim_end(".lua")
.or_else(|_| path.file_name_trim_end(".luau"));
if let Ok(name) = script_name {
match name { match name {
// init scripts are handled elsewhere and should not turn into // init scripts are handled elsewhere and should not turn into
// their own children. // their own children.

View File

@@ -6,6 +6,7 @@ use rbx_reflection::ClassTag;
use crate::{ use crate::{
project::{PathNode, Project, ProjectNode}, project::{PathNode, Project, ProjectNode},
small_string::SmallString,
snapshot::{ snapshot::{
InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule, InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule,
}, },
@@ -67,13 +68,10 @@ pub fn snapshot_project_node(
) -> anyhow::Result<Option<InstanceSnapshot>> { ) -> anyhow::Result<Option<InstanceSnapshot>> {
let project_folder = project_path.parent().unwrap(); let project_folder = project_path.parent().unwrap();
let class_name_from_project = node let class_name_from_project = node.class_name.as_ref().map(|name| SmallString::from(name));
.class_name
.as_ref()
.map(|name| Cow::Owned(name.clone()));
let mut class_name_from_path = None; let mut class_name_from_path = None;
let name = Cow::Owned(instance_name.to_owned()); let name = SmallString::from(instance_name);
let mut properties = HashMap::new(); let mut properties = HashMap::new();
let mut children = Vec::new(); let mut children = Vec::new();
let mut metadata = InstanceMetadata::default(); let mut metadata = InstanceMetadata::default();
@@ -228,7 +226,7 @@ pub fn snapshot_project_node(
_ => {} _ => {}
} }
properties.insert(key.clone(), value); properties.insert(key.into(), value);
} }
// If the user specified $ignoreUnknownInstances, overwrite the existing // If the user specified $ignoreUnknownInstances, overwrite the existing
@@ -262,7 +260,7 @@ pub fn snapshot_project_node(
})) }))
} }
fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<Cow<'static, str>> { fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<SmallString> {
// If className wasn't defined from another source, we may be able // If className wasn't defined from another source, we may be able
// to infer one. // to infer one.
@@ -275,13 +273,13 @@ fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<Cow<'stati
let descriptor = rbx_reflection_database::get().classes.get(name)?; let descriptor = rbx_reflection_database::get().classes.get(name)?;
if descriptor.tags.contains(&ClassTag::Service) { if descriptor.tags.contains(&ClassTag::Service) {
return Some(Cow::Owned(name.to_owned())); return Some(name.into());
} }
} else if parent_class == "StarterPlayer" { } else if parent_class == "StarterPlayer" {
// StarterPlayer has two special members with their own classes. // StarterPlayer has two special members with their own classes.
if name == "StarterPlayerScripts" || name == "StarterCharacterScripts" { if name == "StarterPlayerScripts" || name == "StarterCharacterScripts" {
return Some(Cow::Owned(name.to_owned())); return Some(name.into());
} }
} }

View File

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

View File

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

View File

@@ -11,14 +11,10 @@ metadata:
- /foo - /foo
- /foo/init.meta.json - /foo/init.meta.json
- /foo/init.lua - /foo/init.lua
- /foo/init.luau
- /foo/init.server.lua - /foo/init.server.lua
- /foo/init.server.luau
- /foo/init.client.lua - /foo/init.client.lua
- /foo/init.client.luau
context: {} context: {}
name: foo name: foo
class_name: Folder class_name: Folder
properties: {} properties: {}
children: [] children: []

View File

@@ -11,11 +11,8 @@ metadata:
- /foo - /foo
- /foo/init.meta.json - /foo/init.meta.json
- /foo/init.lua - /foo/init.lua
- /foo/init.luau
- /foo/init.server.lua - /foo/init.server.lua
- /foo/init.server.luau
- /foo/init.client.lua - /foo/init.client.lua
- /foo/init.client.luau
context: {} context: {}
name: foo name: foo
class_name: Folder class_name: Folder
@@ -30,14 +27,10 @@ children:
- /foo/Child - /foo/Child
- /foo/Child/init.meta.json - /foo/Child/init.meta.json
- /foo/Child/init.lua - /foo/Child/init.lua
- /foo/Child/init.luau
- /foo/Child/init.server.lua - /foo/Child/init.server.lua
- /foo/Child/init.server.luau
- /foo/Child/init.client.lua - /foo/Child/init.client.lua
- /foo/Child/init.client.luau
context: {} context: {}
name: Child name: Child
class_name: Folder class_name: Folder
properties: {} properties: {}
children: [] children: []

View File

@@ -1,29 +0,0 @@
---
source: src/snapshot_middleware/json_model.rs
assertion_line: 186
expression: instance_snapshot
---
snapshot_id: ~
metadata:
ignore_unknown_instances: false
instigating_source:
Path: /foo.model.json
relevant_paths:
- /foo.model.json
context: {}
name: foo
class_name: IntValue
properties:
Value:
Int64: 5
children:
- snapshot_id: ~
metadata:
ignore_unknown_instances: false
relevant_paths: []
context: {}
name: The Child
class_name: StringValue
properties: {}
children: []

View File

@@ -21,7 +21,7 @@ pub fn snapshot_txt(
.to_owned(); .to_owned();
let properties = hashmap! { let properties = hashmap! {
"Value".to_owned() => contents_str.into(), "Value".into() => contents_str.into(),
}; };
let meta_path = path.with_file_name(format!("{}.meta.json", name)); let meta_path = path.with_file_name(format!("{}.meta.json", name));

View File

@@ -244,7 +244,7 @@ impl ApiService {
} }
} }
/// If this instance is represented by a script, try to find the correct .lua or .luau /// If this instance is represented by a script, try to find the correct .lua
/// file to open to edit it. /// file to open to edit it.
fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> { fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
match instance.class_name() { match instance.class_name() {
@@ -252,17 +252,16 @@ fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
_ => return None, _ => return None,
} }
// Pick the first listed relevant path that has an extension of .lua or .luau that // Pick the first listed relevant path that has an extension of .lua that
// exists. // exists.
instance instance
.metadata() .metadata()
.relevant_paths .relevant_paths
.iter() .iter()
.find(|path| { .find(|path| {
// We should only ever open Lua or Luau files to be safe. // We should only ever open Lua files to be safe.
match path.extension().and_then(|ext| ext.to_str()) { match path.extension().and_then(|ext| ext.to_str()) {
Some("lua") => {} Some("lua") => {}
Some("luau") => {}
_ => return false, _ => return false,
} }

View File

@@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
session_id::SessionId, session_id::SessionId,
small_string::SmallString,
snapshot::{ snapshot::{
AppliedPatchSet, InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta, RojoTree, AppliedPatchSet, InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta, RojoTree,
}, },
@@ -83,13 +84,13 @@ impl<'a> SubscribeMessage<'a> {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct InstanceUpdate { pub struct InstanceUpdate {
pub id: Ref, pub id: Ref,
pub changed_name: Option<String>, pub changed_name: Option<SmallString>,
pub changed_class_name: Option<String>, pub changed_class_name: Option<SmallString>,
// TODO: Transform from HashMap<String, Option<_>> to something else, since // TODO: Transform from HashMap<_, Option<_>> to something else, since
// null will get lost when decoding from JSON in some languages. // null will get lost when decoding from JSON in some languages.
#[serde(default)] #[serde(default)]
pub changed_properties: HashMap<String, Option<Variant>>, pub changed_properties: HashMap<SmallString, Option<Variant>>,
pub changed_metadata: Option<InstanceMetadata>, pub changed_metadata: Option<InstanceMetadata>,
} }

View File

@@ -1,16 +0,0 @@
{
"name": "attributes",
"tree": {
"$className": "DataModel",
"Workspace": {
"Folder": {
"$className": "Folder",
"$properties": {
"Attributes": {
"Hello": { "Vector3": [1, 2, 3] }
}
}
}
}
}
}

View File

@@ -1,14 +0,0 @@
{
"name": "tags",
"tree": {
"$className": "DataModel",
"Workspace": {
"Folder": {
"$className": "Folder",
"$properties": {
"Tags": ["Hello", "World"]
}
}
}
}
}

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,53 +0,0 @@
---
globals:
FIXME:
args:
- required: false
type: string
FOCUS:
args: []
SKIP:
args: []
afterAll:
args:
- type: function
afterEach:
args:
- type: function
beforeAll:
args:
- type: function
beforeEach:
args:
- type: function
describe:
args:
- type: string
- type: function
describeFOCUS:
args:
- type: string
- type: function
describeSKIP:
args:
- type: string
- type: function
expect:
args:
- type: any
it:
args:
- type: string
- type: function
itFIXME:
args:
- type: string
- type: function
itFOCUS:
args:
- type: string
- type: function
itSKIP:
args:
- type: string
- type: function

View File

@@ -21,7 +21,6 @@ macro_rules! gen_build_tests {
} }
gen_build_tests! { gen_build_tests! {
attributes,
client_in_folder, client_in_folder,
client_init, client_init,
csv_bug_145, csv_bug_145,
@@ -37,13 +36,11 @@ gen_build_tests! {
init_meta_class_name, init_meta_class_name,
init_meta_properties, init_meta_properties,
init_with_children, init_with_children,
issue_546,
json_as_lua, json_as_lua,
json_model_in_folder, json_model_in_folder,
json_model_legacy_name, json_model_legacy_name,
module_in_folder, module_in_folder,
module_init, module_init,
optional,
project_composed_default, project_composed_default,
project_composed_file, project_composed_file,
project_root_name, project_root_name,
@@ -56,6 +53,7 @@ gen_build_tests! {
txt, txt,
txt_in_folder, txt_in_folder,
unresolved_values, unresolved_values,
optional,
weldconstraint, weldconstraint,
} }