From 268197297685cfb7ce9bffdd07b241a466406670 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Fri, 1 Dec 2017 00:53:41 -0800 Subject: [PATCH] Much more robust reconciliation implementation --- plugin/src/Plugin.lua | 84 +-------------------- plugin/src/Reconciler.lua | 155 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 81 deletions(-) create mode 100644 plugin/src/Reconciler.lua diff --git a/plugin/src/Plugin.lua b/plugin/src/Plugin.lua index 4bb1185f..22a12479 100644 --- a/plugin/src/Plugin.lua +++ b/plugin/src/Plugin.lua @@ -2,6 +2,7 @@ local Config = require(script.Parent.Config) local Http = require(script.Parent.Http) local Server = require(script.Parent.Server) local Promise = require(script.Parent.Promise) +local Reconciler = require(script.Parent.Reconciler) local function collectMatch(source, pattern) local result = {} @@ -13,85 +14,6 @@ local function collectMatch(source, pattern) return result end -local function fileToName(filename) - if filename:find("%.server%.lua$") then - return filename:match("^(.-)%.server%.lua$"), "Script" - elseif filename:find("%.client%.lua$") then - return filename:match("^(.-)%.client%.lua$"), "LocalScript" - elseif filename:find("%.lua") then - return filename:match("^(.-)%.lua$"), "ModuleScript" - else - return filename, "StringValue" - end -end - -local function nameToInstance(filename, contents) - local name, className = fileToName(filename) - - local instance = Instance.new(className) - instance.Name = name - - if className:find("Script$") then - instance.Source = contents - else - instance.Value = contents - end - - return instance -end - -local function make(item, name) - if item.type == "dir" then - local instance = Instance.new("Folder") - instance.Name = name - - for childName, child in pairs(item.children) do - make(child, childName).Parent = instance - end - - return instance - elseif item.type == "file" then - return nameToInstance(name, item.contents) - else - error("not implemented") - end -end - -local function write(parent, route, item) - local location = parent - - for index = 1, #route - 1 do - local piece = route[index] - local newLocation = location:FindFirstChild(piece) - - if not newLocation then - newLocation = Instance.new("Folder") - newLocation.Name = piece - newLocation.Parent = location - end - - location = newLocation - end - - local fileName = route[#route] - local name = fileToName(fileName) - - local existing = location:FindFirstChild(name) - - local new - if item then - new = make(item, fileName) - end - - if existing then - existing:Destroy() - end - - if new then - new.Parent = location - end -end - local Plugin = {} Plugin.__index = Plugin @@ -188,14 +110,14 @@ function Plugin:_pull(server, project, routes) local route = routes[index] local partitionName = route[1] local partition = project.partitions[partitionName] - local data = items[index] + local item = items[index] local fullRoute = collectMatch(partition.target, "[^.]+") for i = 2, #route do table.insert(fullRoute, routes[index][i]) end - write(game, fullRoute, data) + Reconciler.reconcileRoute(fullRoute, item) end end diff --git a/plugin/src/Reconciler.lua b/plugin/src/Reconciler.lua new file mode 100644 index 00000000..03373e65 --- /dev/null +++ b/plugin/src/Reconciler.lua @@ -0,0 +1,155 @@ +local Reconciler = {} + +local function itemToName(item, fileName) + if item and item.type == "dir" then + return fileName, "Folder" + elseif item and item.type == "file" or not item then + if fileName:find("%.server%.lua$") then + return fileName:match("^(.-)%.server%.lua$"), "Script" + elseif fileName:find("%.client%.lua$") then + return fileName:match("^(.-)%.client%.lua$"), "LocalScript" + elseif fileName:find("%.lua") then + return fileName:match("^(.-)%.lua$"), "ModuleScript" + else + return fileName, "StringValue" + end + else + error("unknown item type " .. tostring(item.type)) + end +end + +local function setValues(rbx, item, fileName) + local _, className = itemToName(item, fileName) + + if className:find("Script") then + rbx.Source = item.contents + else + rbx.Value = item.contents + end +end + +function Reconciler._reifyShallow(item, fileName) + if item.type == "dir" then + -- TODO: handle init + local rbx = Instance.new("Folder") + rbx.Name = fileName + + return rbx + elseif item.type == "file" then + local objectName, className = itemToName(item, fileName) + + local rbx = Instance.new(className) + rbx.Name = objectName + + setValues(rbx, item, fileName) + + return rbx + else + error("unknown item type " .. tostring(item.type)) + end +end + +function Reconciler._reify(item, fileName) + local rbx = Reconciler._reifyShallow(item, fileName) + + if item.type == "dir" then + for childName, child in pairs(item.children) do + local childRbx = Reconciler._reify(child, childName) + childRbx.Parent = rbx + + -- TODO: handle init + end + end + + return rbx +end + +function Reconciler.reconcile(rbx, item, fileName) + -- Item was deleted! + if not item then + if rbx then + rbx:Destroy() + end + + return + end + + -- Item was created! + if not rbx then + return Reconciler._reify(item, fileName) + end + + if item.type == "dir" then + -- TODO: handle init + + if rbx.ClassName ~= "Folder" then + return Reconciler._reify(item, fileName) + end + + local visitedChildren = {} + + for childFileName, childItem in pairs(item.children) do + local childName = itemToName(childItem, childFileName) + + visitedChildren[childName] = true + + Reconciler.reconcile(rbx:FindFirstChild(childName), childItem, childFileName) + end + + for _, childRbx in ipairs(rbx:GetChildren()) do + -- Child was deleted! + if not visitedChildren[childRbx.Name] then + Reconciler.reconcile(childRbx, nil, nil) + end + end + + return rbx + elseif item.type == "file" then + local _, className = itemToName(item, fileName) + + if rbx.ClassName ~= className then + return Reconciler._reify(item, fileName) + end + + setValues(rbx, item, fileName) + + return rbx + else + error("unknown item type " .. tostring(item.type)) + end +end + +function Reconciler.reconcileRoute(route, item) + local location = game + + for i = 1, #route - 1 do + local piece = route[i] + local newLocation = location:FindFirstChild(piece) + + if not newLocation then + newLocation = Instance.new("Folder") + newLocation.Name = piece + newLocation.Parent = location + end + + location = newLocation + end + + local fileName = route[#route] + + local name = itemToName(item, fileName) + local rbx = location:FindFirstChild(name) + local newRbx = Reconciler.reconcile(rbx, item, fileName) + + if newRbx ~= rbx then + if rbx then + rbx:Destroy() + end + + if newRbx then + newRbx.Parent = location + end + end +end + +return Reconciler