diff --git a/.gitignore b/.gitignore index b11f390d..c360f389 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /site -/target +**/target /scratch-project **/*.rs.bk /server/failed-snapshots/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 741d18ac..96915ad2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,4 +12,7 @@ url = https://github.com/LPGhatguy/roblox-lua-promise.git [submodule "plugin/modules/t"] path = plugin/modules/t - url = https://github.com/osyrisrblx/t.git \ No newline at end of file + url = https://github.com/osyrisrblx/t.git +[submodule "plugin/modules/rbx-dom"] + path = plugin/modules/rbx-dom + url = https://github.com/LPGhatguy/rbx-dom diff --git a/plugin/default.project.json b/plugin/default.project.json index 2cf6eadc..22d46799 100644 --- a/plugin/default.project.json +++ b/plugin/default.project.json @@ -8,6 +8,9 @@ "Roact": { "$path": "modules/roact/lib" }, + "RbxDomLua": { + "$path": "modules/rbx-dom/rbx_dom_lua/src" + }, "Promise": { "$path": "modules/promise/lib" }, diff --git a/plugin/modules/rbx-dom b/plugin/modules/rbx-dom new file mode 160000 index 00000000..42c62061 --- /dev/null +++ b/plugin/modules/rbx-dom @@ -0,0 +1 @@ +Subproject commit 42c620612ce03c4026d3836d9f4c0446410929a7 diff --git a/plugin/place.project.json b/plugin/place.project.json index 9ba8c85b..603b5032 100644 --- a/plugin/place.project.json +++ b/plugin/place.project.json @@ -15,6 +15,9 @@ "Roact": { "$path": "modules/roact/lib" }, + "RbxDomLua": { + "$path": "modules/rbx-dom/rbx_dom_lua/src" + }, "Promise": { "$path": "modules/promise/lib" }, diff --git a/plugin/src/Reconciler.lua b/plugin/src/Reconciler.lua index 58eb4182..772df684 100644 --- a/plugin/src/Reconciler.lua +++ b/plugin/src/Reconciler.lua @@ -2,13 +2,14 @@ local t = require(script.Parent.Parent.t) local InstanceMap = require(script.Parent.InstanceMap) local Logging = require(script.Parent.Logging) -local setCanonicalProperty = require(script.Parent.setCanonicalProperty) -local rojoValueToRobloxValue = require(script.Parent.rojoValueToRobloxValue) +local RbxDomLua = require(script.Parent.Parent.RbxDomLua) local Types = require(script.Parent.Types) local Reconciler = {} Reconciler.__index = Reconciler +local writeInstance = RbxDomLua.CanonicalInstance.writeInstance + function Reconciler.new() local self = { instanceMap = InstanceMap.new(), @@ -27,6 +28,29 @@ function Reconciler:applyUpdate(requestedIds, virtualInstancesById) end end +function Reconciler:mapProperties(virtualProperties, virtualInstancesById) + -- Maps properties from the Rojo format to one rbx-dom-lua uses + local properties = {} + + for propertyName, property in pairs(virtualProperties) do + if property.Type == "BinaryString" then + local binaryString = "" + for _, characterCode in pairs(property.Value) do + binaryString = binaryString .. string.char(characterCode) + end + properties[propertyName] = binaryString + elseif property.Type == "Ref" then + if property.Value ~= nil then + properties[propertyName] = self.instanceMap.fromIds[property.Value] + end + else + properties[propertyName] = property.Value + end + end + + return properties +end + local reconcileSchema = Types.ifEnabled(t.tuple( t.map(t.string, Types.VirtualInstance), t.string, @@ -53,11 +77,9 @@ function Reconciler:reconcile(virtualInstancesById, id, instance) self.instanceMap:insert(id, instance) -- Some instances don't like being named, even if their name already matches - setCanonicalProperty(instance, "Name", virtualInstance.Name) - - for key, value in pairs(virtualInstance.Properties) do - setCanonicalProperty(instance, key, rojoValueToRobloxValue(value)) - end + writeInstance(instance, { + Name = virtualInstance.Name, + }) local existingChildren = instance:GetChildren() @@ -105,6 +127,8 @@ function Reconciler:reconcile(virtualInstancesById, id, instance) end end + writeInstance(instance, self:mapProperties(virtualInstance.Properties)) + -- The root instance of a project won't have a parent, like the DataModel, -- so we need to be careful here. if virtualInstance.Parent ~= nil then @@ -117,7 +141,9 @@ function Reconciler:reconcile(virtualInstancesById, id, instance) -- Some instances, like services, don't like having their Parent -- property poked, even if we're setting it to the same value. - setCanonicalProperty(instance, "Parent", parent) + writeInstance(instance, { + Parent = parent, + }) end return instance @@ -143,18 +169,19 @@ function Reconciler:__reify(virtualInstancesById, id, parent) local virtualInstance = virtualInstancesById[id] local instance = Instance.new(virtualInstance.ClassName) - - for key, value in pairs(virtualInstance.Properties) do - setCanonicalProperty(instance, key, rojoValueToRobloxValue(value)) - end - - setCanonicalProperty(instance, "Name", virtualInstance.Name) + writeInstance(instance, { + Name = virtualInstance.Name, + }) for _, childId in ipairs(virtualInstance.Children) do self:__reify(virtualInstancesById, childId, instance) end - setCanonicalProperty(instance, "Parent", parent) + writeInstance(instance, self:mapProperties(virtualInstance.Properties)) + writeInstance(instance, { + Parent = parent, + }) + self.instanceMap:insert(id, instance) return instance diff --git a/plugin/src/Reconciler.spec.lua b/plugin/src/Reconciler.spec.lua index 794c4d08..da11eb3a 100644 --- a/plugin/src/Reconciler.spec.lua +++ b/plugin/src/Reconciler.spec.lua @@ -1,3 +1,5 @@ +local CollectionService = game:GetService("CollectionService") + local Reconciler = require(script.Parent.Reconciler) return function() @@ -53,8 +55,8 @@ return function() Properties = { Value = { Type = "Int32", - Value = 9 - } + Value = 9, + }, }, }, } @@ -65,6 +67,73 @@ return function() expect(instance.Value).to.equal(9) end) + it("should assign properties that can't just be easily set", function() + local instance = Instance.new("Folder") + + local binaryString = {} + for _, character in pairs(("Tag1\0Tag2"):split("")) do + table.insert(binaryString, character:byte()) + end + + local instanceId = "test-id" + local virtualInstancesById = { + [instanceId] = { + Name = "Folder", + ClassName = "Folder", + Children = {}, + Properties = { + Tags = { + Type = "BinaryString", + Value = binaryString, + }, + }, + }, + } + + local reconciler = Reconciler.new() + reconciler:reconcile(virtualInstancesById, instanceId, instance) + + assert(CollectionService:HasTag(instance, "Tag1")) + assert(CollectionService:HasTag(instance, "Tag2")) + end) + + it("should assign ref properties", function() + local model = Instance.new("Model") + model.Name = "Parent" + + local child = Instance.new("Part") + child.Name = "Child" + child.Parent = model + + local childId = "child-id" + local modelId = "model-id" + + local virtualInstancesById = { + [modelId] = { + Name = "Parent", + ClassName = "Model", + Children = { childId }, + Properties = { + PrimaryPart = { + Type = "Ref", + Value = childId, + }, + }, + }, + + [childId] = { + Name = "Child", + ClassName = "Part", + Children = {}, + Properties = {}, + }, + } + + local reconciler = Reconciler.new() + reconciler:reconcile(virtualInstancesById, modelId, model) + expect(model.PrimaryPart).to.equal(child) + end) + it("should wipe unknown children by default", function() local parent = Instance.new("Folder") parent.Name = "Parent" diff --git a/plugin/src/rojoValueToRobloxValue.lua b/plugin/src/rojoValueToRobloxValue.lua deleted file mode 100644 index b5855b2a..00000000 --- a/plugin/src/rojoValueToRobloxValue.lua +++ /dev/null @@ -1,38 +0,0 @@ -local primitiveTypes = { - Bool = true, - Enum = true, - Float32 = true, - Float64 = true, - Int32 = true, - Int64 = true, - String = true, -} - -local directConstructors = { - CFrame = CFrame.new, - Color3 = Color3.new, - Color3uint8 = Color3.fromRGB, - Rect = Rect.new, - UDim = UDim.new, - UDim2 = UDim2.new, - Vector2 = Vector2.new, - Vector2int16 = Vector2int16.new, - Vector3 = Vector3.new, - Vector3int16 = Vector3int16.new, -} - -local function rojoValueToRobloxValue(value) - if primitiveTypes[value.Type] then - return value.Value - end - - local constructor = directConstructors[value.Type] - if constructor ~= nil then - return constructor(unpack(value.Value)) - end - - local errorMessage = ("The Rojo plugin doesn't know how to handle values of type %q yet!"):format(tostring(value.Type)) - error(errorMessage) -end - -return rojoValueToRobloxValue \ No newline at end of file diff --git a/plugin/src/rojoValueToRobloxValue.spec.lua b/plugin/src/rojoValueToRobloxValue.spec.lua deleted file mode 100644 index fe7cbaec..00000000 --- a/plugin/src/rojoValueToRobloxValue.spec.lua +++ /dev/null @@ -1,40 +0,0 @@ -local rojoValueToRobloxValue = require(script.Parent.rojoValueToRobloxValue) - -return function() - it("should convert primitives", function() - local inputString = { - Type = "String", - Value = "Hello, world!", - } - - local inputFloat32 = { - Type = "Float32", - Value = 12341.512, - } - - expect(rojoValueToRobloxValue(inputString)).to.equal(inputString.Value) - expect(rojoValueToRobloxValue(inputFloat32)).to.equal(inputFloat32.Value) - end) - - it("should convert properties with direct constructors", function() - local inputColor3 = { - Type = "Color3", - Value = {0, 1, 0.5}, - } - local outputColor3 = Color3.new(0, 1, 0.5) - - local inputCFrame = { - Type = "CFrame", - Value = { - 1, 2, 3, - 4, 5, 6, - 7, 8, 9, - 10, 11, 12, - }, - } - local outputCFrame = CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) - - expect(rojoValueToRobloxValue(inputColor3)).to.equal(outputColor3) - expect(rojoValueToRobloxValue(inputCFrame)).to.equal(outputCFrame) - end) -end \ No newline at end of file diff --git a/plugin/src/setCanonicalProperty.lua b/plugin/src/setCanonicalProperty.lua deleted file mode 100644 index 6e5eacd6..00000000 --- a/plugin/src/setCanonicalProperty.lua +++ /dev/null @@ -1,57 +0,0 @@ -local Logging = require(script.Parent.Logging) - ---[[ - Attempts to set a property on the given instance. - - This method deals in terms of what Rojo calls 'canonical properties', which - don't necessarily exist either in serialization or in Lua-reflected APIs, - but may be present in the API dump. - - Ideally, canonical properties map 1:1 with properties we can assign, but in - some cases like LocalizationTable contents and CollectionService tags, we - have to read/write properties a little differently. -]] -local function setCanonicalProperty(instance, key, value) - -- The 'Contents' property of LocalizationTable isn't directly exposed, but - -- has corresponding (deprecated) getters and setters. - if instance.ClassName == "LocalizationTable" and key == "Contents" then - instance:SetContents(value) - return - end - - -- Temporary workaround for fixing issue #141 in this specific case. - if instance.ClassName == "Lighting" and key == "Technology" then - return - end - - -- If we don't have permissions to access this value at all, we can skip it. - local readSuccess, existingValue = pcall(function() - return instance[key] - end) - - if not readSuccess then - -- An error will be thrown if there was a permission issue or if the - -- property doesn't exist. In the latter case, we should tell the user - -- because it's probably their fault. - if existingValue:find("lacking permission") then - Logging.trace("Permission error reading property %s on class %s", tostring(key), instance.ClassName) - return - else - error(("Invalid property %s on class %s: %s"):format(tostring(key), instance.ClassName, existingValue), 2) - end - end - - local writeSuccess, err = pcall(function() - if existingValue ~= value then - instance[key] = value - end - end) - - if not writeSuccess then - error(("Cannot set property %s on class %s: %s"):format(tostring(key), instance.ClassName, err), 2) - end - - return true -end - -return setCanonicalProperty \ No newline at end of file