mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Add CI pipeline documentation and enhance syncback architecture
- Introduced a new CI pipeline documentation file outlining steps for auto-fixing formatting, running linters, building, and testing. - Documented the Rojo syncback architecture, detailing improvements to prevent duplicate file creation and format mismatches. - Updated syncback logic to ensure existing instances are updated instead of creating duplicates, enhancing project file management. - Added tests for encoding instances and handling nested children to improve reliability in syncback operations.
This commit is contained in:
parent
edc09741bf
commit
b368eb2401
46 changed files with 3688 additions and 112 deletions
|
|
@ -156,5 +156,154 @@ return function()
|
|||
folder:Destroy()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("children encoding", function()
|
||||
it("should include children array even when empty", function()
|
||||
local moduleScript = Instance.new("ModuleScript")
|
||||
moduleScript.Name = "TestModule"
|
||||
moduleScript.Source = "return {}"
|
||||
|
||||
local encoded = encodeInstance(moduleScript, "PARENT_ID")
|
||||
|
||||
expect(encoded).to.be.ok()
|
||||
expect(encoded.children).to.be.ok()
|
||||
expect(type(encoded.children)).to.equal("table")
|
||||
expect(#encoded.children).to.equal(0)
|
||||
|
||||
moduleScript:Destroy()
|
||||
end)
|
||||
|
||||
it("should encode ModuleScript with children", function()
|
||||
local parent = Instance.new("ModuleScript")
|
||||
parent.Name = "ParentModule"
|
||||
parent.Source = "return {}"
|
||||
|
||||
local child1 = Instance.new("ModuleScript")
|
||||
child1.Name = "ChildModule1"
|
||||
child1.Source = "return 1"
|
||||
child1.Parent = parent
|
||||
|
||||
local child2 = Instance.new("ModuleScript")
|
||||
child2.Name = "ChildModule2"
|
||||
child2.Source = "return 2"
|
||||
child2.Parent = parent
|
||||
|
||||
local encoded = encodeInstance(parent, "PARENT_ID")
|
||||
|
||||
expect(encoded).to.be.ok()
|
||||
expect(encoded.children).to.be.ok()
|
||||
expect(#encoded.children).to.equal(2)
|
||||
|
||||
-- Verify children are properly encoded
|
||||
local childNames = {}
|
||||
for _, child in encoded.children do
|
||||
childNames[child.name] = true
|
||||
expect(child.className).to.equal("ModuleScript")
|
||||
expect(child.properties.Source).to.be.ok()
|
||||
end
|
||||
expect(childNames["ChildModule1"]).to.equal(true)
|
||||
expect(childNames["ChildModule2"]).to.equal(true)
|
||||
|
||||
parent:Destroy()
|
||||
end)
|
||||
|
||||
it("should encode nested children recursively", function()
|
||||
local root = Instance.new("Folder")
|
||||
root.Name = "Root"
|
||||
|
||||
local level1 = Instance.new("Folder")
|
||||
level1.Name = "Level1"
|
||||
level1.Parent = root
|
||||
|
||||
local level2 = Instance.new("ModuleScript")
|
||||
level2.Name = "Level2"
|
||||
level2.Source = "return 'nested'"
|
||||
level2.Parent = level1
|
||||
|
||||
local encoded = encodeInstance(root, "PARENT_ID")
|
||||
|
||||
expect(encoded).to.be.ok()
|
||||
expect(#encoded.children).to.equal(1)
|
||||
expect(encoded.children[1].name).to.equal("Level1")
|
||||
expect(#encoded.children[1].children).to.equal(1)
|
||||
expect(encoded.children[1].children[1].name).to.equal("Level2")
|
||||
expect(encoded.children[1].children[1].className).to.equal("ModuleScript")
|
||||
|
||||
root:Destroy()
|
||||
end)
|
||||
|
||||
it("should skip children with duplicate names", function()
|
||||
local parent = Instance.new("Folder")
|
||||
parent.Name = "Parent"
|
||||
|
||||
local child1 = Instance.new("ModuleScript")
|
||||
child1.Name = "DuplicateName"
|
||||
child1.Source = "return 1"
|
||||
child1.Parent = parent
|
||||
|
||||
local child2 = Instance.new("ModuleScript")
|
||||
child2.Name = "DuplicateName" -- Same name!
|
||||
child2.Source = "return 2"
|
||||
child2.Parent = parent
|
||||
|
||||
local child3 = Instance.new("ModuleScript")
|
||||
child3.Name = "UniqueName"
|
||||
child3.Source = "return 3"
|
||||
child3.Parent = parent
|
||||
|
||||
local encoded = encodeInstance(parent, "PARENT_ID")
|
||||
|
||||
expect(encoded).to.be.ok()
|
||||
-- Should only have 1 child (UniqueName), duplicates skipped
|
||||
expect(#encoded.children).to.equal(1)
|
||||
expect(encoded.children[1].name).to.equal("UniqueName")
|
||||
|
||||
parent:Destroy()
|
||||
end)
|
||||
|
||||
it("should return empty children array when all children deleted", function()
|
||||
-- Simulate: parent had children, now they're all gone
|
||||
local parent = Instance.new("ModuleScript")
|
||||
parent.Name = "ParentModule"
|
||||
parent.Source = "return {}"
|
||||
-- No children parented
|
||||
|
||||
local encoded = encodeInstance(parent, "PARENT_ID")
|
||||
|
||||
expect(encoded).to.be.ok()
|
||||
expect(encoded.children).to.be.ok()
|
||||
expect(#encoded.children).to.equal(0)
|
||||
|
||||
parent:Destroy()
|
||||
end)
|
||||
|
||||
it("should encode Script with Folder child", function()
|
||||
-- This tests the init.server.luau + child directory scenario
|
||||
local script = Instance.new("Script")
|
||||
script.Name = "EventService"
|
||||
script.Source = "-- Event service"
|
||||
|
||||
local events = Instance.new("Folder")
|
||||
events.Name = "Events"
|
||||
events.Parent = script
|
||||
|
||||
local event1 = Instance.new("ModuleScript")
|
||||
event1.Name = "AcidRainEvent"
|
||||
event1.Source = "return {}"
|
||||
event1.Parent = events
|
||||
|
||||
local encoded = encodeInstance(script, "PARENT_ID")
|
||||
|
||||
expect(encoded).to.be.ok()
|
||||
expect(encoded.className).to.equal("Script")
|
||||
expect(#encoded.children).to.equal(1)
|
||||
expect(encoded.children[1].name).to.equal("Events")
|
||||
expect(encoded.children[1].className).to.equal("Folder")
|
||||
expect(#encoded.children[1].children).to.equal(1)
|
||||
expect(encoded.children[1].children[1].name).to.equal("AcidRainEvent")
|
||||
|
||||
script:Destroy()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
local parentObject = instance.Parent
|
||||
local parentId = instanceMap.fromInstances[parentObject]
|
||||
local previousId = nil
|
||||
while parentObject do
|
||||
while parentObject and parentId do
|
||||
if knownAncestors[parentId] then
|
||||
-- We've already added this ancestor
|
||||
previousId = parentId
|
||||
|
|
|
|||
|
|
@ -175,13 +175,21 @@ local function shouldDeleteChild(virtualInstance, childInstance)
|
|||
return false
|
||||
end
|
||||
|
||||
local function diff(instanceMap, virtualInstances, rootId)
|
||||
local function diff(instanceMap, virtualInstances, rootId, serverInfo)
|
||||
local patch = {
|
||||
removed = {},
|
||||
added = {},
|
||||
updated = {},
|
||||
}
|
||||
|
||||
-- Build a lookup table for visible services (for ignoreHiddenServices check)
|
||||
local visibleServicesSet: { [string]: boolean } = {}
|
||||
if serverInfo and serverInfo.ignoreHiddenServices and serverInfo.visibleServices then
|
||||
for _, serviceName in ipairs(serverInfo.visibleServices) do
|
||||
visibleServicesSet[serviceName] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Count of locations with duplicate-named siblings
|
||||
local skippedDuplicateCount = 0
|
||||
|
||||
|
|
@ -403,6 +411,20 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||
continue
|
||||
end
|
||||
|
||||
-- Skip hidden services when ignoreHiddenServices is enabled.
|
||||
-- Services are direct children of game (DataModel), and if they're
|
||||
-- not in the visible services list, they weren't synced back to disk,
|
||||
-- so we shouldn't mark them for deletion during forward sync.
|
||||
if serverInfo and serverInfo.ignoreHiddenServices and instance == game then
|
||||
if not visibleServicesSet[childInstance.Name] then
|
||||
Log.trace(
|
||||
"Skipping hidden service {} (ignoreHiddenServices is enabled)",
|
||||
childInstance.Name
|
||||
)
|
||||
continue
|
||||
end
|
||||
end
|
||||
|
||||
-- This is an existing instance not present in the virtual DOM.
|
||||
-- We can mark it for deletion unless the user has asked us not
|
||||
-- to delete unknown stuff. Scripts are always eligible for deletion
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
return function()
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local InstanceMap = require(script.Parent.Parent.InstanceMap)
|
||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
|
||||
|
|
@ -164,15 +161,7 @@ return function()
|
|||
assert(isEmpty(patch.updated))
|
||||
end)
|
||||
|
||||
--[[
|
||||
Because rbx_dom_lua resolves non-canonical properties to their canonical
|
||||
variants, this test does not work as intended.
|
||||
|
||||
Instead, heat_xml is diffed with Heat, the canonical property variant,
|
||||
and a patch trying to assign to heat_xml is generated. This is
|
||||
incorrect, but will require more invasive changes to fix later.
|
||||
]]
|
||||
itFIXME("should ignore unreadable properties", function()
|
||||
it("should ignore unreadable properties", function()
|
||||
local knownInstances = InstanceMap.new()
|
||||
local virtualInstances = {
|
||||
ROOT = {
|
||||
|
|
@ -194,8 +183,6 @@ return function()
|
|||
|
||||
local ok, patch = diff(knownInstances, virtualInstances, "ROOT")
|
||||
|
||||
Log.warn("{:#?}", patch)
|
||||
|
||||
assert(ok, tostring(patch))
|
||||
|
||||
assert(isEmpty(patch.removed))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,18 @@ local function getProperty(instance, propertyName)
|
|||
})
|
||||
end
|
||||
|
||||
-- If the property name we were given is not the canonical name, treat it
|
||||
-- as unknown. This handles serialization-only aliases like `heat_xml`
|
||||
-- which resolve to `Heat` - we should skip these and let the canonical
|
||||
-- property name be used instead.
|
||||
if descriptor.name ~= propertyName then
|
||||
return false,
|
||||
Error.new(Error.UnknownProperty, {
|
||||
className = instance.ClassName,
|
||||
propertyName = propertyName,
|
||||
})
|
||||
end
|
||||
|
||||
if descriptor.scriptability == "None" or descriptor.scriptability == "Write" then
|
||||
return false,
|
||||
Error.new(Error.UnreadableProperty, {
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ function Reconciler:hydrate(virtualInstances, rootId, rootInstance)
|
|||
return result
|
||||
end
|
||||
|
||||
function Reconciler:diff(virtualInstances, rootId)
|
||||
function Reconciler:diff(virtualInstances, rootId, serverInfo)
|
||||
Timer.start("Reconciler:diff")
|
||||
local success, result = diff(self.__instanceMap, virtualInstances, rootId)
|
||||
local success, result = diff(self.__instanceMap, virtualInstances, rootId, serverInfo)
|
||||
Timer.stop()
|
||||
|
||||
return success, result
|
||||
|
|
|
|||
|
|
@ -612,7 +612,7 @@ function ServeSession:__computeInitialPatch(serverInfo)
|
|||
Log.trace("Computing changes that plugin needs to make to catch up to server...")
|
||||
self:setLoadingText("Finding differences between server and Studio...")
|
||||
local success, catchUpPatch =
|
||||
self.__reconciler:diff(readResponseBody.instances, serverInfo.rootInstanceId, game)
|
||||
self.__reconciler:diff(readResponseBody.instances, serverInfo.rootInstanceId, serverInfo)
|
||||
|
||||
if not success then
|
||||
Log.error("Could not compute a diff to catch up to the Rojo server: {:#?}", catchUpPatch)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue