forked from rojo-rbx/rojo
Two issues prevented --git-since from working correctly during live sync: 1. Server: File changes weren't detected because git-filtered project nodes had empty relevant_paths, so the change processor couldn't map VFS events back to tree instances. Fixed by registering $path directories and the project folder in relevant_paths even when filtered. 2. Plugin: When a previously-filtered file was first acknowledged, it appeared as an ADD patch. The plugin created a new instance instead of adopting the existing one in Studio, causing duplicates. Fixed by checking for untracked children with matching Name+ClassName before calling Instance.new. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
183 lines
5.6 KiB
Lua
183 lines
5.6 KiB
Lua
--[[
|
|
"Reifies" a virtual DOM, constructing a real DOM with the same shape.
|
|
]]
|
|
|
|
local invariant = require(script.Parent.Parent.invariant)
|
|
local PatchSet = require(script.Parent.Parent.PatchSet)
|
|
local setProperty = require(script.Parent.setProperty)
|
|
local decodeValue = require(script.Parent.decodeValue)
|
|
|
|
--[[
|
|
Add the given ID and all of its descendants in virtualInstances to the given
|
|
PatchSet, marked for addition.
|
|
]]
|
|
local function addAllToPatch(patchSet, virtualInstances, id)
|
|
local virtualInstance = virtualInstances[id]
|
|
patchSet.added[id] = virtualInstance
|
|
|
|
for _, childId in ipairs(virtualInstance.Children) do
|
|
addAllToPatch(patchSet, virtualInstances, childId)
|
|
end
|
|
end
|
|
|
|
function reifyInstance(deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
|
|
-- Create an empty patch that will be populated with any parts of this reify
|
|
-- that could not happen, like instances that couldn't be created and
|
|
-- properties that could not be assigned.
|
|
local unappliedPatch = PatchSet.newEmpty()
|
|
|
|
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
|
|
|
|
return unappliedPatch
|
|
end
|
|
|
|
--[[
|
|
Inner function that defines the core routine.
|
|
]]
|
|
function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, id, parentInstance)
|
|
local virtualInstance = virtualInstances[id]
|
|
|
|
if virtualInstance == nil then
|
|
invariant("Cannot reify an instance not present in virtualInstances\nID: {}", id)
|
|
end
|
|
|
|
-- Before creating a new instance, check if the parent already has an
|
|
-- untracked child with the same Name and ClassName. This enables "late
|
|
-- adoption" of instances that exist in Studio but weren't in the initial
|
|
-- Rojo tree (e.g., when using --git-since filtering). Without this,
|
|
-- newly acknowledged files would create duplicate instances.
|
|
local adoptedExisting = false
|
|
local instance = nil
|
|
|
|
for _, child in ipairs(parentInstance:GetChildren()) do
|
|
local accessSuccess, name, className = pcall(function()
|
|
return child.Name, child.ClassName
|
|
end)
|
|
|
|
if accessSuccess
|
|
and name == virtualInstance.Name
|
|
and className == virtualInstance.ClassName
|
|
and instanceMap.fromInstances[child] == nil
|
|
then
|
|
instance = child
|
|
adoptedExisting = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if not adoptedExisting then
|
|
-- Instance.new can fail if we're passing in something that can't be
|
|
-- created, like a service, something enabled with a feature flag, or
|
|
-- something that requires higher security than we have.
|
|
local createSuccess
|
|
createSuccess, instance = pcall(Instance.new, virtualInstance.ClassName)
|
|
|
|
if not createSuccess then
|
|
addAllToPatch(unappliedPatch, virtualInstances, id)
|
|
return
|
|
end
|
|
end
|
|
|
|
-- TODO: Can this fail? Previous versions of Rojo guarded against this, but
|
|
-- the reason why was uncertain.
|
|
instance.Name = virtualInstance.Name
|
|
|
|
-- Track all of the properties that we've failed to assign to this instance.
|
|
local unappliedProperties = {}
|
|
|
|
for propertyName, virtualValue in pairs(virtualInstance.Properties) do
|
|
-- Because refs may refer to instances that we haven't constructed yet,
|
|
-- we defer applying any ref properties until all instances are created.
|
|
if next(virtualValue) == "Ref" then
|
|
table.insert(deferredRefs, {
|
|
id = id,
|
|
instance = instance,
|
|
propertyName = propertyName,
|
|
virtualValue = virtualValue,
|
|
})
|
|
continue
|
|
end
|
|
|
|
local decodeSuccess, value = decodeValue(virtualValue, instanceMap)
|
|
if not decodeSuccess then
|
|
unappliedProperties[propertyName] = virtualValue
|
|
continue
|
|
end
|
|
|
|
local setPropertySuccess = setProperty(instance, propertyName, value)
|
|
if not setPropertySuccess then
|
|
unappliedProperties[propertyName] = virtualValue
|
|
end
|
|
end
|
|
|
|
-- If there were any properties that we failed to assign, push this into our
|
|
-- unapplied patch as an update that would need to be applied.
|
|
if next(unappliedProperties) ~= nil then
|
|
table.insert(unappliedPatch.updated, {
|
|
id = id,
|
|
changedProperties = unappliedProperties,
|
|
})
|
|
end
|
|
|
|
for _, childId in ipairs(virtualInstance.Children) do
|
|
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, childId, instance)
|
|
end
|
|
|
|
if not adoptedExisting then
|
|
instance.Parent = parentInstance
|
|
end
|
|
instanceMap:insert(id, instance)
|
|
end
|
|
|
|
function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
|
local function markFailed(id, propertyName, virtualValue)
|
|
-- If there is already an updated entry in the unapplied patch for this
|
|
-- ref, use the existing one. This could match other parts of the
|
|
-- instance that failed to be created, or even just other refs that
|
|
-- failed to apply.
|
|
--
|
|
-- This is important for instances like selectable GUI objects, which
|
|
-- have many similar referent properties.
|
|
for _, existingUpdate in ipairs(unappliedPatch.updated) do
|
|
if existingUpdate.id == id then
|
|
existingUpdate.changedProperties[propertyName] = virtualValue
|
|
return
|
|
end
|
|
end
|
|
|
|
-- We didn't find an existing entry that matched, so push a new entry
|
|
-- into our unapplied patch.
|
|
table.insert(unappliedPatch.updated, {
|
|
id = id,
|
|
changedProperties = {
|
|
[propertyName] = virtualValue,
|
|
},
|
|
})
|
|
end
|
|
|
|
for _, entry in deferredRefs do
|
|
local _, refId = next(entry.virtualValue)
|
|
|
|
if refId == nil then
|
|
continue
|
|
end
|
|
|
|
local targetInstance = instanceMap.fromIds[refId]
|
|
|
|
if targetInstance == nil then
|
|
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
|
continue
|
|
end
|
|
|
|
local setPropertySuccess = setProperty(entry.instance, entry.propertyName, targetInstance)
|
|
if not setPropertySuccess then
|
|
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
|
end
|
|
end
|
|
end
|
|
|
|
return {
|
|
reifyInstance = reifyInstance,
|
|
applyDeferredRefs = applyDeferredRefs,
|
|
}
|