--[[ "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, }