add gs
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_dash"]["dash"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
--[[
|
||||
The Bridge interface allows code in different plugins / data-models / libraries to communicate
|
||||
with each other.
|
||||
|
||||
The BindableEventBridge implementation uses a BindableEvent in the data model to act as a
|
||||
message passing interface.
|
||||
]]
|
||||
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Source = script.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local join = Dash.join
|
||||
local class = Dash.class
|
||||
local forEach = Dash.forEach
|
||||
local insert = table.insert
|
||||
|
||||
local BINDABLE_EVENT_NAME = "DeveloperTools"
|
||||
|
||||
local BindableEventBridge = class("BindableEventBridge", function(parent: Instance, noCreate: boolean)
|
||||
local id = HttpService:GenerateGUID()
|
||||
return {
|
||||
id = id,
|
||||
connections = {},
|
||||
noCreate = noCreate
|
||||
}
|
||||
end)
|
||||
|
||||
function BindableEventBridge:_createEvent(parent: Instance)
|
||||
if self.noCreate then
|
||||
return nil
|
||||
end
|
||||
local event = Instance.new("BindableEvent")
|
||||
event.Name = BINDABLE_EVENT_NAME
|
||||
event.Parent = parent
|
||||
return event
|
||||
end
|
||||
|
||||
function BindableEventBridge:_init(parent: Instance)
|
||||
self.event = parent:FindFirstChild(BINDABLE_EVENT_NAME) or self:_createEvent(parent)
|
||||
end
|
||||
|
||||
function BindableEventBridge:send(message)
|
||||
local outMessage = join(message, {
|
||||
fromBridgeId = self.id
|
||||
})
|
||||
if self.event then
|
||||
self.event:Fire(outMessage)
|
||||
end
|
||||
end
|
||||
|
||||
function BindableEventBridge:connect(listener)
|
||||
local function onEvent(message)
|
||||
if message.fromBridgeId ~= self.id then
|
||||
listener(message)
|
||||
end
|
||||
end
|
||||
if self.event then
|
||||
local connection = self.event.Event:Connect(onEvent)
|
||||
insert(self.connections, connection)
|
||||
return connection
|
||||
end
|
||||
end
|
||||
|
||||
function BindableEventBridge:destroy()
|
||||
forEach(self.connections, function(connection)
|
||||
connection:Disconnect()
|
||||
end)
|
||||
end
|
||||
|
||||
return BindableEventBridge
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
--[[
|
||||
The CoreGuiDebugInterface class is used to attach the DeveloperInspector to the CoreGui.
|
||||
]]
|
||||
local Source = script.Parent.Parent
|
||||
|
||||
local BindableEventBridge = require(Source.Classes.BindableEventBridge)
|
||||
local DebugInterface = require(Source.Classes.DebugInterface)
|
||||
local CoreGui = game:GetService("CoreGui")
|
||||
|
||||
local CoreGuiDebugInterface = DebugInterface:extend("CoreGuiDebugInterface", function(appName: string, guiOptions)
|
||||
local bridge = BindableEventBridge.new(CoreGui)
|
||||
local interface = DebugInterface.new("CoreGui", appName, {bridge})
|
||||
if guiOptions then
|
||||
spawn(function()
|
||||
-- The children may not be available synchronously
|
||||
interface:setGuiOptions({
|
||||
rootInstance = CoreGui:WaitForChild(guiOptions.rootInstance, 10),
|
||||
pickerParent = CoreGui:WaitForChild(guiOptions.pickerParent, 10),
|
||||
rootPath = guiOptions.rootPath or {guiOptions.rootInstance}
|
||||
})
|
||||
end)
|
||||
else
|
||||
interface:setGuiOptions({
|
||||
rootInstance = CoreGui,
|
||||
pickerParent = CoreGui,
|
||||
rootPath = {}
|
||||
})
|
||||
end
|
||||
return interface
|
||||
end)
|
||||
|
||||
return CoreGuiDebugInterface
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
--[[
|
||||
The DebugInterface class controls interactions between consumers of the DeveloperTools library
|
||||
and the DeveloperTools inspector plugin itself.
|
||||
]]
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Source = script.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local class = Dash.class
|
||||
local forEach = Dash.forEach
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
local EventName = require(Source.EventName)
|
||||
local PluginEventRouter = require(Source.Classes.PluginEventRouter)
|
||||
local RoactInspectorWorker = require(Source.RoactInspector.Classes.RoactInspectorWorker)
|
||||
|
||||
local DebugInterface = class("DebugInterface", function(sourceKind: string, sourceName: string, bridges)
|
||||
return {
|
||||
sourceId = HttpService:GenerateGUID(),
|
||||
sourceKind = sourceKind,
|
||||
sourceName = sourceName,
|
||||
bridges = bridges,
|
||||
routers = {},
|
||||
targets = {},
|
||||
workers = {},
|
||||
connectionsForListener = {},
|
||||
outboundBridgeForBridgeId = {}
|
||||
}
|
||||
end)
|
||||
|
||||
function DebugInterface:addRoactTree(targetName: string, roactTree)
|
||||
assert(typeof(targetName) == "string", "targetName must be a string")
|
||||
assert(roactTree, "roactTree must be defined")
|
||||
|
||||
self:_addTarget(targetName, function(targetId: string, toBridgeId: string)
|
||||
return RoactInspectorWorker.new(self, targetId, toBridgeId, roactTree)
|
||||
end)
|
||||
end
|
||||
|
||||
function DebugInterface:addPluginRouter(plugin)
|
||||
insert(self.routers, PluginEventRouter.new(self.sourceName, plugin, self.bridges))
|
||||
end
|
||||
|
||||
function DebugInterface:_connectTargets()
|
||||
-- Ensure that GetTargets is only connected to once.
|
||||
-- If targets have already been added we don't need to add another listener
|
||||
if #self.targets > 0 then
|
||||
return
|
||||
end
|
||||
self:_connect({
|
||||
eventName = EventName.GetTargets,
|
||||
onEvent = function(message)
|
||||
self:_send({
|
||||
eventName = EventName.ShowTargets,
|
||||
toBridgeId = message.fromBridgeId,
|
||||
sourceId = self.sourceId,
|
||||
sourceName = self.sourceName,
|
||||
sourceKind = self.sourceKind,
|
||||
targets = self.targets,
|
||||
})
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
function DebugInterface:_send(message)
|
||||
if not (game:GetService("StudioService"):HasInternalPermission()) then
|
||||
return
|
||||
end
|
||||
if not message.toBridgeId then
|
||||
forEach(self.bridges, function(bridge)
|
||||
bridge:send(message)
|
||||
end)
|
||||
else
|
||||
local bridge = self.outboundBridgeForBridgeId[message.toBridgeId]
|
||||
if not bridge then
|
||||
error(("[DeveloperTools] No bridge to other bridge %s"):format(message.toBridgeId))
|
||||
end
|
||||
bridge:send(message)
|
||||
end
|
||||
end
|
||||
|
||||
function DebugInterface:_connect(listener)
|
||||
if not (game:GetService("StudioService"):HasInternalPermission()) then
|
||||
return
|
||||
end
|
||||
local connections = {}
|
||||
self.connectionsForListener[listener] = connections
|
||||
forEach(self.bridges, function(bridge)
|
||||
local newListener = function(message)
|
||||
self.outboundBridgeForBridgeId[message.fromBridgeId] = bridge
|
||||
local matchesEvent = listener.eventName == nil or listener.eventName == message.eventName
|
||||
local matchesBridge = listener.bridgeId == nil or listener.bridgeId == message.toBridgeId
|
||||
local matchesTarget = listener.targetId == nil or listener.targetId == message.toTargetId
|
||||
if matchesEvent and matchesBridge and matchesTarget then
|
||||
listener.onEvent(message)
|
||||
end
|
||||
end
|
||||
local connection = bridge:connect(newListener)
|
||||
insert(connections, connection)
|
||||
end)
|
||||
end
|
||||
|
||||
function DebugInterface:_disconnect(listener)
|
||||
local connections = self.connectionsForListener[listener]
|
||||
forEach(connections, function(connection)
|
||||
connection:Disconnect()
|
||||
end)
|
||||
end
|
||||
|
||||
function DebugInterface:_addTarget(targetName: string, getWorker)
|
||||
self:_connectTargets()
|
||||
local id = HttpService:GenerateGUID()
|
||||
local target = {
|
||||
id = id,
|
||||
name = targetName,
|
||||
}
|
||||
self.targets[id] = target
|
||||
self:_connect({
|
||||
targetId = id,
|
||||
eventName = EventName.AttachTarget,
|
||||
onEvent = function(message)
|
||||
local worker = self.workers[id] or getWorker(id, message.fromBridgeId)
|
||||
self.workers[id] = worker
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
function DebugInterface:_removeWorker(id)
|
||||
local worker = self.workers[id]
|
||||
if worker then
|
||||
worker:destroy()
|
||||
end
|
||||
self.workers[id] = nil
|
||||
end
|
||||
|
||||
function DebugInterface:setGuiOptions(guiOptions)
|
||||
self.rootInstance = guiOptions.rootInstance
|
||||
self.pickerParent = guiOptions.pickerParent
|
||||
self.rootPath = guiOptions.rootPath
|
||||
self.rootPrefix = guiOptions.rootPrefix
|
||||
end
|
||||
|
||||
function DebugInterface:destroy()
|
||||
forEach(self.bridges, function(bridge)
|
||||
bridge:destroy()
|
||||
end)
|
||||
forEach(self.routers, function(router)
|
||||
router:destroy()
|
||||
end)
|
||||
forEach(self.workers, function(worker)
|
||||
worker:destroy()
|
||||
end)
|
||||
end
|
||||
|
||||
return DebugInterface
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
--[[
|
||||
The InspectorDebugInterface class is used by the DeveloperInspector plugin to attach to other
|
||||
sources of information.
|
||||
]]
|
||||
local Source = script.Parent.Parent
|
||||
|
||||
local BindableEventBridge = require(Source.Classes.BindableEventBridge)
|
||||
local DebugInterface = require(Source.Classes.DebugInterface)
|
||||
local RoactInspectorApi = require(Source.RoactInspector.Classes.RoactInspectorApi)
|
||||
local EventName = require(Source.EventName)
|
||||
|
||||
local InspectorDebugInterface = DebugInterface:extend("InspectorDebugInterface", function(handlers)
|
||||
local pluginBridge = BindableEventBridge.new(game:GetService("StudioService"))
|
||||
local coreGuiBridge = BindableEventBridge.new(game:GetService("CoreGui"))
|
||||
local libraryBridge = BindableEventBridge.new(game:GetService("ReplicatedStorage"), true)
|
||||
local interface = DebugInterface.new("Inspector", "Inspector", {pluginBridge, coreGuiBridge, libraryBridge})
|
||||
interface.handlers = handlers
|
||||
return interface
|
||||
end)
|
||||
|
||||
function InspectorDebugInterface:_init()
|
||||
self:_connectInspector()
|
||||
end
|
||||
|
||||
function InspectorDebugInterface:getTargetApi()
|
||||
return self.targetApi
|
||||
end
|
||||
|
||||
function InspectorDebugInterface:closeTargetApi()
|
||||
if self.targetApi then
|
||||
self.targetApi:close()
|
||||
end
|
||||
end
|
||||
|
||||
function InspectorDebugInterface:_connectInspector()
|
||||
self:_connect({
|
||||
eventName = EventName.ShowTargets,
|
||||
onEvent = function(message)
|
||||
self.handlers.onAddTargets(message)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
function InspectorDebugInterface:getTargets()
|
||||
self:_send({
|
||||
eventName = EventName.GetTargets
|
||||
})
|
||||
end
|
||||
|
||||
function InspectorDebugInterface:attachRoactTree(bridgeId, targetId)
|
||||
local roactInspectorApi = RoactInspectorApi.new(self, bridgeId, targetId)
|
||||
roactInspectorApi:attach(self.handlers.RoactInspector)
|
||||
self.targetApi = roactInspectorApi
|
||||
return self.targetApi
|
||||
end
|
||||
|
||||
return InspectorDebugInterface
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
--[[
|
||||
The LibraryDebugInterface class is used to attach the DeveloperInspector to a library being
|
||||
developed inside Studio in the ReplicatedStorage service.
|
||||
]]
|
||||
local Source = script.Parent.Parent
|
||||
|
||||
local BindableEventBridge = require(Source.Classes.BindableEventBridge)
|
||||
local DebugInterface = require(Source.Classes.DebugInterface)
|
||||
|
||||
local LibraryDebuggerInterface = DebugInterface:extend("LibraryDebuggerInterface", function(libraryName: string, guiOptions)
|
||||
local bridge = BindableEventBridge.new(game:GetService("CoreGui"))
|
||||
local interface = DebugInterface.new("Library", libraryName, {bridge})
|
||||
interface:setGuiOptions(guiOptions or {
|
||||
rootInstance = game:GetService("ReplicatedStorage")
|
||||
})
|
||||
return interface
|
||||
end)
|
||||
|
||||
return LibraryDebuggerInterface
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
--[[
|
||||
The PluginDebugInterface class is used to attach the DeveloperInspector to another plugin.
|
||||
]]
|
||||
local Source = script.Parent.Parent
|
||||
|
||||
local BindableEventBridge = require(Source.Classes.BindableEventBridge)
|
||||
local DebugInterface = require(Source.Classes.DebugInterface)
|
||||
local RobloxPluginGuiService = game:GetService("RobloxPluginGuiService")
|
||||
|
||||
local PluginDebugInterface = DebugInterface:extend("PluginDebugInterface", function(pluginName: string, plugin, rootInstance: Instance?)
|
||||
local bridge = BindableEventBridge.new(game:GetService("StudioService"))
|
||||
local interface = DebugInterface.new("Plugin", pluginName, {bridge})
|
||||
if rootInstance then
|
||||
interface:setGuiOptions({
|
||||
rootInstance = rootInstance
|
||||
})
|
||||
else
|
||||
-- Default to finding the root instance under the plugin gui service
|
||||
spawn(function()
|
||||
-- Wait for the UI to populate
|
||||
local gui = RobloxPluginGuiService:WaitForChild(pluginName, 10)
|
||||
if gui then
|
||||
interface:setGuiOptions({
|
||||
rootInstance = gui
|
||||
})
|
||||
end
|
||||
end)
|
||||
end
|
||||
return interface
|
||||
end)
|
||||
|
||||
return PluginDebugInterface
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
--[[
|
||||
The PluginEventBridge class provides a means for a plugin that has code in a different
|
||||
data model to be inspected by the DeveloperTools plugin.
|
||||
]]
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Source = script.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local class = Dash.class
|
||||
local join = Dash.join
|
||||
local forEach = Dash.forEach
|
||||
local insert = table.insert
|
||||
|
||||
local PLUGIN_EVENT_NAME = "DeveloperTools"
|
||||
|
||||
local PluginEventBridge = class("PluginEventBridge", function(plugin)
|
||||
return {
|
||||
id = HttpService:GenerateGUID(),
|
||||
plugin = plugin,
|
||||
connections = {}
|
||||
}
|
||||
end)
|
||||
|
||||
function PluginEventBridge:send(message)
|
||||
local outMessage = join(message, {
|
||||
fromBridgeId = self.id
|
||||
})
|
||||
-- print("[DeveloperTools] Send (Plugin):", pretty(outMessage))
|
||||
self.plugin:Invoke(PLUGIN_EVENT_NAME, outMessage)
|
||||
end
|
||||
|
||||
function PluginEventBridge:connect(listener)
|
||||
local function onEvent(message)
|
||||
if message.fromBridgeId ~= self.id then
|
||||
listener(message)
|
||||
end
|
||||
end
|
||||
local connection = self.plugin:OnInvoke(PLUGIN_EVENT_NAME, onEvent)
|
||||
insert(self.connections, connection)
|
||||
end
|
||||
|
||||
function PluginEventBridge:destroy()
|
||||
forEach(self.connections, function(connection)
|
||||
connection:Disconnect()
|
||||
end)
|
||||
end
|
||||
|
||||
return PluginEventBridge
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
--[[
|
||||
The PluginEventRouter class provides a means for a plugin that has code in a different
|
||||
data model to be inspected by the DeveloperTools plugin.
|
||||
]]
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Source = script.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local join = Dash.join
|
||||
local class = Dash.class
|
||||
local forEach = Dash.forEach
|
||||
|
||||
local PLUGIN_EVENT_NAME = "DeveloperTools"
|
||||
|
||||
|
||||
local PluginEventRouter = class("PluginEventRouter", function(sourceName, plugin, bridges)
|
||||
return {
|
||||
routerId = HttpService:GenerateGUID(),
|
||||
sourceName = sourceName,
|
||||
plugin = plugin,
|
||||
bridges = bridges,
|
||||
-- A list of ids of PluginEvent bridges that have invoked our event
|
||||
outboundBridgeIds = {}
|
||||
}
|
||||
end)
|
||||
|
||||
function PluginEventRouter:_init()
|
||||
self.connection = self.plugin:OnInvoke(PLUGIN_EVENT_NAME, function(message)
|
||||
-- Ignore messages from ourselves
|
||||
if message.fromRouter then
|
||||
return
|
||||
end
|
||||
-- Mark the bridge that this message originated from as accessible
|
||||
self.outboundBridgeIds[message.fromBridgeId] = true
|
||||
local outMessage = join(message, {
|
||||
sourceName = self.sourceName
|
||||
})
|
||||
forEach(self.bridges, function(bridge)
|
||||
bridge:send(outMessage)
|
||||
end)
|
||||
end)
|
||||
local function onEvent(message)
|
||||
-- Only route messages that have no particular destination
|
||||
-- or to bridges we know already exist behind our event
|
||||
local outMessage = join(message, {
|
||||
fromRouter = true
|
||||
})
|
||||
-- print("[DeveloperTools] Route (Plugin)", pretty(outMessage))
|
||||
self.plugin:Invoke(PLUGIN_EVENT_NAME, outMessage)
|
||||
end
|
||||
forEach(self.bridges, function(bridge)
|
||||
bridge:connect(onEvent)
|
||||
end)
|
||||
end
|
||||
|
||||
function PluginEventRouter:destroy()
|
||||
self.connection:Disconnect()
|
||||
end
|
||||
|
||||
return PluginEventRouter
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
--[[
|
||||
The StandalonePluginDebugInterface class is used to attach the DeveloperInspector to a standalone plugin.
|
||||
]]
|
||||
local Source = script.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local PluginEventBridge = require(Source.Classes.PluginEventBridge)
|
||||
local DebugInterface = require(Source.Classes.DebugInterface)
|
||||
|
||||
local StandalonePluginDebugInterface = DebugInterface:extend("StandalonePluginDebugInterface", function(pluginName: string, plugin, guiOptions)
|
||||
local bridge = PluginEventBridge.new(plugin)
|
||||
local interface = DebugInterface.new("StandalonePlugin", pluginName, {bridge})
|
||||
interface:setGuiOptions(guiOptions)
|
||||
return interface
|
||||
end)
|
||||
|
||||
return StandalonePluginDebugInterface
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
--[[
|
||||
The TargetApi class is a base for methods that the inspector can use to spawn messages to
|
||||
other targets.
|
||||
]]
|
||||
local Source = script.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local EventName = require(Source.EventName)
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local join = Dash.join
|
||||
local class = Dash.class
|
||||
|
||||
local TargetApi = class("TargetApi", function(debugInterface, bridgeId, targetId)
|
||||
return {
|
||||
debugInterface = debugInterface,
|
||||
bridgeId = bridgeId,
|
||||
targetId = targetId
|
||||
}
|
||||
end)
|
||||
|
||||
function TargetApi:close()
|
||||
self:_send({
|
||||
eventName = EventName.CloseTarget
|
||||
})
|
||||
end
|
||||
|
||||
function TargetApi:_send(message)
|
||||
local newMessage = join(message, {
|
||||
toBridgeId = self.bridgeId,
|
||||
toTargetId = self.targetId,
|
||||
})
|
||||
self.debugInterface:_send(newMessage)
|
||||
end
|
||||
|
||||
function TargetApi:_connect(listener)
|
||||
local newListener = join(listener, {
|
||||
fromTargetId = self.targetId,
|
||||
})
|
||||
self.debugInterface:_connect(newListener)
|
||||
end
|
||||
|
||||
function TargetApi:attach()
|
||||
self:_send({
|
||||
eventName = EventName.AttachTarget,
|
||||
})
|
||||
end
|
||||
|
||||
return TargetApi
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
--[[
|
||||
The TargetWorker class is a base for functionality that tracks and notifies the inspector of
|
||||
any changes to a local state value.
|
||||
]]
|
||||
local Source = script.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
local EventName= require(Source.EventName)
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local class = Dash.class
|
||||
local forEach = Dash.forEach
|
||||
local join = Dash.join
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
local TargetWorker = class("TargetWorker", function(debugInterface, targetId, toBridgeId)
|
||||
return {
|
||||
targetId = targetId,
|
||||
toBridgeId = toBridgeId,
|
||||
debugInterface = debugInterface,
|
||||
listeners = {}
|
||||
}
|
||||
end)
|
||||
|
||||
function TargetWorker:connectEvents()
|
||||
self:connect({
|
||||
eventName = EventName.CloseTarget,
|
||||
onEvent = function(message)
|
||||
self.debugInterface:_removeWorker(self.targetId)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
function TargetWorker:connect(listener)
|
||||
local newListener = join(listener, {
|
||||
targetId = self.targetId,
|
||||
})
|
||||
self.debugInterface:_connect(newListener)
|
||||
insert(self.listeners, newListener)
|
||||
end
|
||||
|
||||
function TargetWorker:send(message)
|
||||
local newMessage = join(message, {
|
||||
fromTargetId = self.targetId,
|
||||
toBridgeId = self.toBridgeId,
|
||||
})
|
||||
self.debugInterface:_send(newMessage)
|
||||
end
|
||||
|
||||
function TargetWorker:destroy()
|
||||
forEach(self.listeners, function(listener)
|
||||
self.debugInterface:_disconnect(listener)
|
||||
end)
|
||||
end
|
||||
|
||||
return TargetWorker
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
local Source = script.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local freeze = Dash.freeze
|
||||
|
||||
return freeze("EventName", {
|
||||
CloseTarget = "CloseTarget",
|
||||
GetTargets = "GetTargets",
|
||||
ShowTargets = "ShowTargets",
|
||||
AttachTarget = "AttachTarget",
|
||||
RoactInspector = freeze("EventName.RoactInspector", {
|
||||
GetChildren = "RoactInspector.GetChildren",
|
||||
ShowChildren = "RoactInspector.ShowChildren",
|
||||
GetBranch = "RoactInspector.GetBranch",
|
||||
ShowBranch = "RoactInspector.ShowBranch",
|
||||
GetFields = "RoactInspector.GetFields",
|
||||
ShowFields = "RoactInspector.ShowFields",
|
||||
SetPicking = "RoactInspector.SetPicking",
|
||||
PickInstance = "RoactInspector.PickInstance",
|
||||
Highlight = "RoactInspector.GetHighlight",
|
||||
Dehighlight = "RoactInspector.Dehighlight",
|
||||
}, true)
|
||||
}, true)
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
--[[
|
||||
The FieldWatcher class is provided with a root table using setRoot and paths to watch for
|
||||
nested changes at. It periodically polls to check if any of the values being watched have
|
||||
changed, and calls the onFieldsChange handler with a list of changed paths.
|
||||
]]
|
||||
local Source = script.Parent.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
local getChildAtKey = require(Source.RoactInspector.Utils.getChildAtKey)
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local Types = Dash.Types
|
||||
local assign = Dash.assign
|
||||
local class = Dash.class
|
||||
local collect = Dash.collect
|
||||
local copy = Dash.copy
|
||||
local keys = Dash.keys
|
||||
local pretty = Dash.pretty
|
||||
local reduce = Dash.reduce
|
||||
local shallowEqual = Dash.shallowEqual
|
||||
|
||||
local insert = table.insert
|
||||
local sort = table.sort
|
||||
|
||||
local POLL_DELAY = 0.5
|
||||
|
||||
type Path = Types.Array<any>
|
||||
|
||||
local FieldWatcher = class("FieldWatcher", function(onFieldsChanged)
|
||||
return {
|
||||
onFieldsChanged = onFieldsChanged,
|
||||
}
|
||||
end)
|
||||
|
||||
function FieldWatcher:_init()
|
||||
-- A periodic check for changes
|
||||
self.onPoll = function()
|
||||
if self.root and self.polling then
|
||||
self:_checkFields()
|
||||
delay(POLL_DELAY, self.onPoll)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Start polling for changes
|
||||
function FieldWatcher:monitor()
|
||||
if not self.polling then
|
||||
self.polling = true
|
||||
spawn(self.onPoll)
|
||||
end
|
||||
end
|
||||
|
||||
-- Determine if any fields have changed
|
||||
function FieldWatcher:_checkFields()
|
||||
-- Collect a map of paths which have had their values changed
|
||||
local changedEntries = collect(self.activeFields, function(path: Path, value)
|
||||
local newValue = self:walk(self.root, path)
|
||||
-- Perform a one-deep comparison as any deeper changes will be monitored by a separate path
|
||||
if not shallowEqual(value, newValue) then
|
||||
return path, newValue
|
||||
end
|
||||
end)
|
||||
-- Update the values being listened to
|
||||
assign(self.activeFields, changedEntries)
|
||||
-- Check for changes and fire the event handler
|
||||
local changedPaths = keys(changedEntries)
|
||||
if #changedPaths > 0 then
|
||||
self.onFieldsChanged(changedPaths)
|
||||
end
|
||||
end
|
||||
|
||||
function FieldWatcher:collect(table, depth: number, path)
|
||||
if typeof(table) ~= "table" then
|
||||
return {}
|
||||
end
|
||||
local fields = collect(table, function(key, value)
|
||||
-- Permit numbers to preserve numeric ordering of numbers (rather than 1, 10, 11, 2, 3...)
|
||||
local name = typeof(key) == "number" and key or tostring(key)
|
||||
-- The path to a nested value uses number or string representations of the keys indexing
|
||||
-- into each table. While it is possible to have name conflicts between the two
|
||||
-- representations or sibling keys, they will hopefully be rare, as complex data types
|
||||
-- typically stringify to a pointer ref in Lua.
|
||||
local childPath = copy(path)
|
||||
insert(childPath, name)
|
||||
local children = typeof(value) == "table" and depth > 0 and self:collect(value, depth - 1, childPath) or {}
|
||||
return name, {
|
||||
Name = name,
|
||||
Summary = pretty(value, {depth = 2, arrayLength = true}),
|
||||
Path = childPath,
|
||||
Children = children
|
||||
}
|
||||
end)
|
||||
return fields
|
||||
end
|
||||
|
||||
-- Access a nested child of _value_ addressed by _path_.
|
||||
function FieldWatcher:walk(value: Types.Table, path: Path)
|
||||
-- Guard against invalid accesses resulting from stale paths or user-defined assertions.
|
||||
local ok, value = pcall(function()
|
||||
return reduce(path, function(current, key)
|
||||
return getChildAtKey(current, key)
|
||||
end, self.root)
|
||||
end)
|
||||
if not ok then
|
||||
warn(("Cannot walk path %s: %s "):format(pretty(path), value))
|
||||
value = nil
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
-- Update the root table being watched, from which paths are addressed from.
|
||||
function FieldWatcher:setRoot(root: Types.Table)
|
||||
self.activeFields = {}
|
||||
self.root = root
|
||||
self:monitor()
|
||||
end
|
||||
|
||||
-- Add a new path to be listened to, made up of an array of keys addressing descendants.
|
||||
-- If the child at the path or any of its children shallow change, an event will be fired.
|
||||
function FieldWatcher:addPath(path: Path)
|
||||
local value = self:walk(self.root, path)
|
||||
-- Store a copy so mutations to the existing value don't mutate the keys we are
|
||||
-- observing, allowing the shallowEqual utility to work its magic
|
||||
self.activeFields[path] = value and copy(value) or {}
|
||||
end
|
||||
|
||||
function FieldWatcher:destroy()
|
||||
self.root = nil
|
||||
self.polling = false
|
||||
end
|
||||
|
||||
return FieldWatcher
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
--[[
|
||||
The InstancePicker allows a user to hover over UI instances and choose one to be inspected in the
|
||||
DeveloperInspector plugin.
|
||||
]]
|
||||
local Source = script.Parent.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local class = Dash.class
|
||||
local format = Dash.format
|
||||
local reduce = Dash.reduce
|
||||
|
||||
local ZERO_UDIM2 = UDim2.fromScale(0, 0)
|
||||
local PICK_DELAY = 0.2
|
||||
|
||||
-- Arbitrary number larger than DevFramework FOCUSED_ZINDEX
|
||||
local PICKER_ZINDEX = 1100000
|
||||
|
||||
local function tweenUDim2(prevValue, nextValue, progress)
|
||||
local scaleX = (prevValue.X.Scale * (1 - progress) + nextValue.X.Scale * progress)
|
||||
local offsetX = (prevValue.X.Offset * (1 - progress) + nextValue.X.Offset * progress)
|
||||
local scaleY = (prevValue.Y.Scale * (1 - progress) + nextValue.Y.Scale * progress)
|
||||
local offsetY = (prevValue.Y.Offset * (1 - progress) + nextValue.Y.Offset * progress)
|
||||
return UDim2.new(scaleX, offsetX, scaleY, offsetY)
|
||||
end
|
||||
|
||||
local function createPickerArea()
|
||||
local pickerArea = Instance.new("ImageButton")
|
||||
pickerArea.Name = "InspectorHover"
|
||||
pickerArea.Active = true
|
||||
pickerArea.AutoButtonColor = false
|
||||
pickerArea.BorderSizePixel = 1
|
||||
pickerArea.BorderColor3 = Color3.fromRGB(0, 0, 0)
|
||||
pickerArea.BackgroundColor3 = Color3.fromRGB(220, 230, 255)
|
||||
pickerArea.BackgroundTransparency = 0.2
|
||||
pickerArea.ZIndex = PICKER_ZINDEX
|
||||
return pickerArea
|
||||
end
|
||||
|
||||
local function createHighlightArea()
|
||||
local highlightArea = Instance.new("Frame")
|
||||
highlightArea.Name = "InspectorHover"
|
||||
highlightArea.BorderSizePixel = 1
|
||||
highlightArea.BorderColor3 = Color3.fromRGB(0, 0, 0)
|
||||
highlightArea.BackgroundColor3 = Color3.fromRGB(220, 230, 255)
|
||||
highlightArea.BackgroundTransparency = 0.2
|
||||
highlightArea.ZIndex = PICKER_ZINDEX
|
||||
|
||||
local dimensions = Instance.new("TextLabel")
|
||||
dimensions.Name = "Dimensions"
|
||||
dimensions.Size = UDim2.new(0, 60, 0, 24)
|
||||
dimensions.Parent = highlightArea
|
||||
dimensions.BorderColor3 = Color3.fromRGB(0, 0, 0)
|
||||
dimensions.BorderSizePixel = 1
|
||||
dimensions.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
|
||||
dimensions.BackgroundTransparency = 0.2
|
||||
dimensions.ZIndex = PICKER_ZINDEX + 1
|
||||
|
||||
return highlightArea
|
||||
end
|
||||
|
||||
local InstancePicker = class("InstancePicker", function(debugInterface, onSelect)
|
||||
return {
|
||||
debugInterface = debugInterface,
|
||||
onSelect = onSelect,
|
||||
active = false,
|
||||
connection = nil,
|
||||
selectedObject = nil,
|
||||
pickerArea = createPickerArea(),
|
||||
highlightArea = createHighlightArea(),
|
||||
selectedTime = os.clock(),
|
||||
nextPosition = ZERO_UDIM2,
|
||||
nextSize = ZERO_UDIM2,
|
||||
prevPosition = ZERO_UDIM2,
|
||||
prevSize = ZERO_UDIM2,
|
||||
}
|
||||
end)
|
||||
|
||||
function InstancePicker:getRoot()
|
||||
return self.debugInterface.rootInstance
|
||||
end
|
||||
|
||||
function InstancePicker:getPickerParent()
|
||||
return self.debugInterface.pickerParent or self:getRoot()
|
||||
end
|
||||
|
||||
function InstancePicker:getRelativeMousePosition()
|
||||
local root = self:getRoot()
|
||||
if not root then
|
||||
return nil
|
||||
end
|
||||
if root:IsA("PluginGui") then
|
||||
return root:GetRelativeMousePosition()
|
||||
else
|
||||
local mouse = game:GetService("Players").LocalPlayer:GetMouse()
|
||||
return Vector2.new(mouse.X, mouse.Y)
|
||||
end
|
||||
end
|
||||
|
||||
function InstancePicker:_init()
|
||||
self.pickerArea.Activated:Connect(function()
|
||||
self.onSelect(self.selectedObject)
|
||||
end)
|
||||
end
|
||||
|
||||
function InstancePicker:setActive(active: boolean)
|
||||
self.active = active
|
||||
local root = self:getRoot()
|
||||
if not root then
|
||||
return
|
||||
end
|
||||
if active then
|
||||
self.pickerArea.Parent = self:getPickerParent()
|
||||
local onCalculate
|
||||
onCalculate = function()
|
||||
if not self.active then
|
||||
return
|
||||
end
|
||||
self:calculateFramePosition()
|
||||
delay(PICK_DELAY, onCalculate)
|
||||
end
|
||||
onCalculate()
|
||||
self.connection = RunService.RenderStepped:Connect(function()
|
||||
self:updateFrame()
|
||||
end)
|
||||
elseif self.connection then
|
||||
self.connection:Disconnect()
|
||||
self.connection = nil
|
||||
self.pickerArea.Parent = nil
|
||||
end
|
||||
end
|
||||
|
||||
function InstancePicker:updateFrame()
|
||||
local progress = math.min(1, (os.clock() - self.selectedTime) / 0.1)
|
||||
self.pickerArea.Position = tweenUDim2(self.prevPosition, self.nextPosition, progress)
|
||||
self.pickerArea.Size = tweenUDim2(self.prevSize, self.nextSize, progress)
|
||||
end
|
||||
|
||||
function InstancePicker:intersectMouse(root: Instance, mousePosition: Vector2)
|
||||
local descendants = root:GetDescendants()
|
||||
local result = reduce(descendants, function(current, instance: Instance)
|
||||
if instance == self.pickerArea then
|
||||
return current
|
||||
end
|
||||
local intersects = false
|
||||
if instance:IsA("GuiBase2d") then
|
||||
local mousePosition = mousePosition - instance.AbsolutePosition
|
||||
intersects = mousePosition.X >= 0
|
||||
and mousePosition.X <= instance.AbsoluteSize.X
|
||||
and mousePosition.Y >= 0
|
||||
and mousePosition.Y <= instance.AbsoluteSize.Y
|
||||
end
|
||||
if instance:IsA("GuiObject") and not instance.Visible then
|
||||
intersects = false
|
||||
end
|
||||
if intersects then
|
||||
local area = instance.AbsoluteSize.X * instance.AbsoluteSize.Y
|
||||
if current.minArea > area then
|
||||
current.minArea = area
|
||||
current.instance = instance
|
||||
end
|
||||
end
|
||||
return current
|
||||
end, {
|
||||
minArea = math.huge
|
||||
})
|
||||
return result.instance
|
||||
end
|
||||
|
||||
function InstancePicker:calculateFramePosition()
|
||||
local root = self:getRoot()
|
||||
local mousePosition = self:getRelativeMousePosition()
|
||||
if not root or not mousePosition then
|
||||
return
|
||||
end
|
||||
if mousePosition ~= self.mousePosition then
|
||||
self.mousePosition = mousePosition
|
||||
local foundObject = self:intersectMouse(root, mousePosition)
|
||||
if foundObject then
|
||||
if foundObject ~= self.selectedObject then
|
||||
self.selectedObject = foundObject
|
||||
self.prevPosition = UDim2.fromOffset(self.pickerArea.AbsolutePosition.X, self.pickerArea.AbsolutePosition.Y)
|
||||
self.prevSize = UDim2.fromOffset(self.pickerArea.AbsoluteSize.X, self.pickerArea.AbsoluteSize.Y)
|
||||
self.nextPosition = UDim2.fromOffset(foundObject.AbsolutePosition.X, foundObject.AbsolutePosition.Y)
|
||||
self.nextSize = UDim2.fromOffset(foundObject.AbsoluteSize.X, foundObject.AbsoluteSize.Y)
|
||||
self.selectedTime = os.clock()
|
||||
end
|
||||
else
|
||||
self.selectedObject = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function InstancePicker:highlight(instance: Instance)
|
||||
-- Some instances may not have sizes so guard for error
|
||||
pcall(function()
|
||||
self.highlightArea.Parent = self:getPickerParent()
|
||||
self.highlightArea.Position = UDim2.fromOffset(instance.AbsolutePosition.X, instance.AbsolutePosition.Y)
|
||||
self.highlightArea.Size = UDim2.fromOffset(instance.AbsoluteSize.X, instance.AbsoluteSize.Y)
|
||||
local dimensions = self.highlightArea:FindFirstChild("Dimensions")
|
||||
dimensions.Text = format("{X}x{Y}", instance.AbsoluteSize)
|
||||
end)
|
||||
end
|
||||
|
||||
function InstancePicker:dehighlight()
|
||||
self.highlightArea.Parent = nil
|
||||
end
|
||||
|
||||
function InstancePicker:destroy()
|
||||
if self.connection then
|
||||
self.connection:Disconnect()
|
||||
end
|
||||
self.pickerArea:Destroy()
|
||||
end
|
||||
|
||||
return InstancePicker
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
--[[
|
||||
The RoactInspectorApi class manages the interface used by the inspector to discover and get
|
||||
notified of changes to the Roact tree it is attached to.
|
||||
]]
|
||||
local Source = script.Parent.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local EventName = require(Source.EventName)
|
||||
local TargetApi = require(Source.Classes.TargetApi)
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local append = Dash.append
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
local RoactInspectorApi = TargetApi:extend("RoactInspectorApi")
|
||||
|
||||
type Path = Types.Array<any>
|
||||
|
||||
function RoactInspectorApi:attach(handlers)
|
||||
self.handlers = handlers
|
||||
TargetApi.attach(self)
|
||||
self:_connect({
|
||||
eventName = EventName.RoactInspector.ShowChildren,
|
||||
onEvent = function(message)
|
||||
self.handlers.onUpdateInstances(message.path, message.children, message.updatedIndexes)
|
||||
end
|
||||
})
|
||||
self:_connect({
|
||||
eventName = EventName.RoactInspector.ShowBranch,
|
||||
onEvent = function(message)
|
||||
self.handlers.onUpdateBranch(message.path, message.branch)
|
||||
end
|
||||
})
|
||||
self:_connect({
|
||||
eventName = EventName.RoactInspector.ShowFields,
|
||||
onEvent = function(message)
|
||||
self.handlers.onUpdateFields(message.path, message.nodeIndex, message.fieldPath, message.fields)
|
||||
end
|
||||
})
|
||||
self:_connect({
|
||||
eventName = EventName.RoactInspector.PickInstance,
|
||||
onEvent = function(message)
|
||||
self.handlers.onPickInstance(message.path)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorApi:getChildren(path: Path)
|
||||
self:_send({
|
||||
eventName = EventName.RoactInspector.GetChildren,
|
||||
path = path
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorApi:getRoot()
|
||||
self:getChildren({})
|
||||
end
|
||||
|
||||
function RoactInspectorApi:getBranch(path: Path)
|
||||
self:_send({
|
||||
eventName = EventName.RoactInspector.GetBranch,
|
||||
path = path
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorApi:getFields(path: Path, nodeIndex: number, fieldPath: Path)
|
||||
self:_send({
|
||||
eventName = EventName.RoactInspector.GetFields,
|
||||
path = path,
|
||||
nodeIndex = nodeIndex,
|
||||
fieldPath = fieldPath
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorApi:setPicking(isPicking: boolean)
|
||||
self:_send({
|
||||
eventName = EventName.RoactInspector.SetPicking,
|
||||
isPicking = isPicking
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorApi:highlight(path: Path)
|
||||
self:_send({
|
||||
eventName = EventName.RoactInspector.Highlight,
|
||||
path = path
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorApi:dehighlight()
|
||||
self:_send({
|
||||
eventName = EventName.RoactInspector.Dehighlight,
|
||||
})
|
||||
end
|
||||
|
||||
return RoactInspectorApi
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
--[[
|
||||
The RoactInspectorWorker class manages the serialization of a roact tree across the bridge,
|
||||
notifies the inspector of any changes to the tree, and allows the inspector to make
|
||||
modifications to the tree.
|
||||
]]
|
||||
local Source = script.Parent.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
|
||||
local getChildAtKey = require(Source.RoactInspector.Utils.getChildAtKey)
|
||||
|
||||
local TargetWorker = require(Source.Classes.TargetWorker)
|
||||
local EventName = require(Source.EventName)
|
||||
local RoactTreeWatcher = require(Source.RoactInspector.Classes.RoactTreeWatcher)
|
||||
local InstancePicker = require(Source.RoactInspector.Classes.InstancePicker)
|
||||
local FieldWatcher = require(Source.RoactInspector.Classes.FieldWatcher)
|
||||
|
||||
local Selection = game:GetService("Selection")
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local Types = Dash.Types
|
||||
local map = Dash.map
|
||||
local forEach = Dash.forEach
|
||||
local last = Dash.last
|
||||
local pretty = Dash.pretty
|
||||
local reduce = Dash.reduce
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
type Path = Types.Array<any>
|
||||
|
||||
local RoactInspectorWorker = TargetWorker:extend("RoactInspectorWorker", function(debugInterface, targetId, toBridgeId, tree)
|
||||
local worker = TargetWorker.new(debugInterface, targetId, toBridgeId)
|
||||
worker.tree = tree
|
||||
return worker
|
||||
end)
|
||||
|
||||
function RoactInspectorWorker:_init()
|
||||
self.treeWatcher = RoactTreeWatcher.new(self.debugInterface, self.tree, function(changedPath, changedIndexes)
|
||||
self:showChildren(changedPath, changedIndexes)
|
||||
end)
|
||||
self.treeWatcher:monitor()
|
||||
|
||||
self.fieldWatcher = FieldWatcher.new(function(changedPaths)
|
||||
self:showFields({})
|
||||
end)
|
||||
|
||||
self.picker = InstancePicker.new(self.debugInterface, function(instance)
|
||||
return self:pickInstance(instance)
|
||||
end)
|
||||
|
||||
self:connectEvents()
|
||||
end
|
||||
|
||||
function RoactInspectorWorker:connectEvents()
|
||||
TargetWorker.connectEvents(self)
|
||||
self:connect({
|
||||
eventName = EventName.RoactInspector.GetChildren,
|
||||
onEvent = function(message)
|
||||
self:showChildren(message.path)
|
||||
end
|
||||
})
|
||||
self:connect({
|
||||
eventName = EventName.RoactInspector.GetBranch,
|
||||
onEvent = function(message)
|
||||
self:showBranch(message.path)
|
||||
end
|
||||
})
|
||||
self:connect({
|
||||
eventName = EventName.RoactInspector.GetFields,
|
||||
onEvent = function(message)
|
||||
self.currentPath = message.path
|
||||
self.currentNodeIndex = message.nodeIndex
|
||||
self:showFields(message.fieldPath or {})
|
||||
end
|
||||
})
|
||||
self:connect({
|
||||
eventName = EventName.RoactInspector.Highlight,
|
||||
onEvent = function(message)
|
||||
local node = self.treeWatcher:getNode(message.path)
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
local hostNode = self.treeWatcher:getHostNode(node)
|
||||
if hostNode and hostNode.hostObject then
|
||||
self.picker:highlight(hostNode.hostObject)
|
||||
else
|
||||
self.picker:dehighlight()
|
||||
end
|
||||
end
|
||||
})
|
||||
self:connect({
|
||||
eventName = EventName.RoactInspector.Dehighlight,
|
||||
onEvent = function(message)
|
||||
self.picker:dehighlight()
|
||||
end
|
||||
})
|
||||
self:connect({
|
||||
eventName = EventName.RoactInspector.SetPicking,
|
||||
onEvent = function(message)
|
||||
self.picker:setActive(message.isPicking)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorWorker:getNodeInfo(node)
|
||||
local source = ""
|
||||
local link = ""
|
||||
if node.currentElement.source then
|
||||
local lines = node.currentElement.source:split("\n")
|
||||
if lines[1] then
|
||||
source = lines[1]
|
||||
link = lines[1]:match("[A-Za-z0-9_]+%.[A-Za-z0-9_]+:[0-9]+") or ""
|
||||
end
|
||||
end
|
||||
return {
|
||||
Name = self.treeWatcher:getNodeName(node),
|
||||
Source = source,
|
||||
Link = link,
|
||||
Icon = self.treeWatcher:getNodeIcon(node)
|
||||
}
|
||||
end
|
||||
|
||||
function RoactInspectorWorker:pickInstance(instance: Instance)
|
||||
self.picker:setActive(false)
|
||||
|
||||
local path = self.treeWatcher:getPath(instance)
|
||||
local currentPath = {}
|
||||
|
||||
-- Gather all the children for these instances and update
|
||||
forEach(path, function(key)
|
||||
insert(currentPath, key)
|
||||
self:showChildren(currentPath)
|
||||
end)
|
||||
|
||||
-- Pick the instance based on the id path to reach it
|
||||
self:send({
|
||||
eventName = EventName.RoactInspector.PickInstance,
|
||||
path = path
|
||||
})
|
||||
|
||||
self:showBranch(path)
|
||||
end
|
||||
|
||||
function RoactInspectorWorker:showChildren(path, updatedIndexes: Types.Array<number>?)
|
||||
local node = self.treeWatcher:getNode(path)
|
||||
if not node then
|
||||
warn("[DeveloperInspector - Roact] Missing path " .. pretty(path))
|
||||
return
|
||||
end
|
||||
self.treeWatcher:watchPath(path)
|
||||
-- Generate at depth two to get grandchildren, so tree rows with children
|
||||
-- display with a toggle arrow.
|
||||
local children = self.treeWatcher:getChildren(path, node, 2)
|
||||
self:send({
|
||||
eventName = EventName.RoactInspector.ShowChildren,
|
||||
path = path,
|
||||
children = children,
|
||||
updatedIndexes = updatedIndexes
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorWorker:showBranch(path)
|
||||
local nodes = self.treeWatcher:getNodes(path)
|
||||
if not nodes then
|
||||
return
|
||||
end
|
||||
local hostNode = last(nodes)
|
||||
if hostNode and hostNode.hostObject then
|
||||
Selection:Set({hostNode.hostObject})
|
||||
end
|
||||
local branch = map(nodes, function(node)
|
||||
return self:getNodeInfo(node)
|
||||
end)
|
||||
self:send({
|
||||
eventName = EventName.RoactInspector.ShowBranch,
|
||||
path = path,
|
||||
branch = branch
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorWorker:showFields(fieldPath: Path)
|
||||
local nodes = self.treeWatcher:getNodes(self.currentPath)
|
||||
if not nodes then
|
||||
return
|
||||
end
|
||||
local node = nodes[self.currentNodeIndex]
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
local container = node.instance or node.currentElement
|
||||
|
||||
self.fieldWatcher:setRoot(container)
|
||||
self.fieldWatcher:addPath(fieldPath)
|
||||
|
||||
-- Safely walk through the container to the correct descendant
|
||||
local fieldRoot = reduce(fieldPath, function(table, key)
|
||||
local ok, child = pcall(function()
|
||||
return getChildAtKey(table, key)
|
||||
end)
|
||||
if ok then
|
||||
return child
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end, container)
|
||||
|
||||
if fieldRoot == nil then
|
||||
return
|
||||
end
|
||||
|
||||
self:send({
|
||||
eventName = EventName.RoactInspector.ShowFields,
|
||||
path = self.currentPath,
|
||||
nodeIndex = self.currentNodeIndex,
|
||||
fieldPath = fieldPath,
|
||||
fields = self.fieldWatcher:collect(fieldRoot, 2, fieldPath)
|
||||
})
|
||||
end
|
||||
|
||||
function RoactInspectorWorker:destroy()
|
||||
TargetWorker.destroy(self)
|
||||
self.picker:destroy()
|
||||
self.treeWatcher:destroy()
|
||||
self.fieldWatcher:destroy()
|
||||
self.treeWatcher = nil
|
||||
end
|
||||
|
||||
return RoactInspectorWorker
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
--[[
|
||||
The RoactTreeWatcher class walks a roact tree and watches for any changes to the nodes.
|
||||
]]
|
||||
local Source = script.Parent.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
local getSymbol = require(Source.RoactInspector.Utils.getSymbol)
|
||||
local getChildAtKey = require(Source.RoactInspector.Utils.getChildAtKey)
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local Types = Dash.Types
|
||||
local append = Dash.append
|
||||
local class = Dash.class
|
||||
local collectArray = Dash.collectArray
|
||||
local pick = Dash.pick
|
||||
local keys = Dash.keys
|
||||
local last = Dash.last
|
||||
local map = Dash.map
|
||||
local mapOne = Dash.mapOne
|
||||
local reduce = Dash.reduce
|
||||
local reverse = Dash.reverse
|
||||
local slice = Dash.slice
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
type Path = Types.Array<any>
|
||||
|
||||
local RoactTreeWatcher = class("RoactTreeWatcher", function(debugInterface, tree, onPathChanged)
|
||||
return {
|
||||
debugInterface = debugInterface,
|
||||
tree = tree,
|
||||
onPathChanged = onPathChanged,
|
||||
cachedRoot = {
|
||||
branchData = {},
|
||||
childNodes = {}
|
||||
}
|
||||
}
|
||||
end)
|
||||
|
||||
local POLL_DELAY = 0.25
|
||||
|
||||
function RoactTreeWatcher:_init()
|
||||
-- A periodic check for changes
|
||||
self.onPoll = function()
|
||||
if self.tree then
|
||||
self:_checkNodes()
|
||||
delay(POLL_DELAY, self.onPoll)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:getRootNode()
|
||||
return getSymbol(self.tree, "InternalData").rootNode
|
||||
end
|
||||
|
||||
-- Start polling for changes
|
||||
function RoactTreeWatcher:monitor()
|
||||
spawn(self.onPoll)
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:_checkNodes()
|
||||
local root = self:getRootNode()
|
||||
self:_checkNode(root, self.cachedRoot, {})
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:_checkNode(node, cachedNode, path)
|
||||
local branch = self:_getBranchNodes(node)
|
||||
local updatedIndexes = collectArray(branch, function(index, branchNode)
|
||||
local nodeData = cachedNode.branchData[index]
|
||||
local container = branchNode.instance or branchNode.currentElement
|
||||
if not nodeData or container.props ~= nodeData.props or container.state ~= nodeData.state then
|
||||
cachedNode.branchData[index] = {
|
||||
props = container.props,
|
||||
state = container.state
|
||||
}
|
||||
return index
|
||||
end
|
||||
end)
|
||||
if #updatedIndexes > 0 then
|
||||
self.onPathChanged(path, updatedIndexes)
|
||||
end
|
||||
local hostNode = self:getHostNode(node)
|
||||
cachedNode.childNodes = pick(cachedNode.childNodes, function(cachedChildNode, key)
|
||||
local childPath = append({}, path, {key})
|
||||
local childNode = getChildAtKey(hostNode.children, key)
|
||||
if childNode then
|
||||
self:_checkNode(childNode, cachedChildNode, childPath)
|
||||
return true
|
||||
else
|
||||
-- The node has been removed
|
||||
return false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:getNode(path: Path)
|
||||
local root = self:getRootNode()
|
||||
local hostNode = self:getHostNode(root)
|
||||
-- Walk down the node tree by the path provided
|
||||
return reduce(path, function(node, key: any)
|
||||
if node and node.children then
|
||||
local childNode = getChildAtKey(node.children, key)
|
||||
return childNode and self:getHostNode(childNode) or nil
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end, hostNode)
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:watchPath(path: Path)
|
||||
local node = self:getRootNode()
|
||||
-- Store references to the current nodes in the tree in a nested structure keyed by the path.
|
||||
-- If the data in these nodes change reference value in the source tree, we know that they
|
||||
-- have been updated and re-rendered.
|
||||
local cachedNode = self.cachedRoot
|
||||
cachedNode.branchData = self:_getBranchData(node)
|
||||
reduce(path, function(currentNode, key: any)
|
||||
if not currentNode then
|
||||
return nil
|
||||
end
|
||||
cachedNode.childNodes[key] = cachedNode.childNodes[key] or {
|
||||
childNodes = {}
|
||||
}
|
||||
cachedNode = cachedNode.childNodes[key]
|
||||
local childNode = getChildAtKey(currentNode.children, key)
|
||||
if not childNode then
|
||||
return nil
|
||||
end
|
||||
cachedNode.branchData = self:_getBranchData(childNode)
|
||||
return self:getHostNode(childNode)
|
||||
end, self:getHostNode(node))
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:_getBranchData(topNode)
|
||||
local branch = self:_getBranchNodes(topNode)
|
||||
return map(branch, function(node)
|
||||
local container = node.instance or node.currentElement
|
||||
return {
|
||||
props = container.props,
|
||||
state = container.state
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:getChildren(path: Path, node, depth: number)
|
||||
-- Return nil to indicate the tree has been truncated, rather that there are 0 children.
|
||||
if depth == 0 then
|
||||
return nil
|
||||
end
|
||||
local hostNode = self:getHostNode(node)
|
||||
if not hostNode then
|
||||
return nil
|
||||
end
|
||||
local children = map(hostNode.children, function(child, key)
|
||||
local childPath = append({}, path, {key})
|
||||
local hostObject = self:getHostNode(child).hostObject
|
||||
local icon = hostObject and hostObject.ClassName or "Branch"
|
||||
return {
|
||||
Name = typeof(key) == "number" and key or tostring(key),
|
||||
Icon = icon,
|
||||
Children = self:getChildren(childPath, child, depth - 1),
|
||||
Path = childPath
|
||||
}
|
||||
end)
|
||||
return children
|
||||
end
|
||||
|
||||
-- Head to the bottom of the current branch, optionally moving through any fragments or portals.
|
||||
function RoactTreeWatcher:getHostNode(node, jumpBranch: boolean?)
|
||||
local child = getSymbol(node.children, "UseParentKey")
|
||||
while child do
|
||||
node = child
|
||||
child = getSymbol(node.children, "UseParentKey")
|
||||
-- If node isn't in branch, try jumping to the next one
|
||||
if not child and jumpBranch then
|
||||
if self:isFragment(node) then
|
||||
child = node.children[1]
|
||||
elseif self:isPortal(node) then
|
||||
child = mapOne(node.children)
|
||||
end
|
||||
end
|
||||
end
|
||||
return node
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:getRootPath()
|
||||
local node = self:getRootNode()
|
||||
local child = getSymbol(node.children, "UseParentKey")
|
||||
local path = {}
|
||||
while child do
|
||||
node = child
|
||||
child = getSymbol(node.children, "UseParentKey")
|
||||
-- If node isn't in branch, try jumping to the next one
|
||||
if not child then
|
||||
if self:isFragment(node) then
|
||||
child = node.children[1]
|
||||
insert(path, 1)
|
||||
elseif self:isPortal(node) then
|
||||
local childKeys = keys(node.children)
|
||||
insert(path, childKeys[1])
|
||||
child = node.children[childKeys[1]]
|
||||
end
|
||||
end
|
||||
end
|
||||
return slice(path, 0, -1)
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:isFragment(node)
|
||||
return node.currentElement and node.currentElement.elements
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:isPortal(node)
|
||||
return node.currentElement and tostring(node.currentElement.component) == "Symbol(Portal)"
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:isFunction(node)
|
||||
return node.currentElement and typeof(node.currentElement.component) == "function"
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:isHost(node)
|
||||
return node.currentElement and typeof(node.currentElement.component) == "string"
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:getNodes(path: Path)
|
||||
-- Split last key off from path
|
||||
local frontPath = slice(path, 1, -1)
|
||||
local lastKey = last(path)
|
||||
-- Get the parent of the path provided
|
||||
local node = self:getNode(frontPath)
|
||||
if not node then
|
||||
return nil
|
||||
end
|
||||
local currentChild = getChildAtKey(node.children, lastKey)
|
||||
return self:_getBranchNodes(currentChild)
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:_getBranchNodes(node)
|
||||
-- Build up the node branch by walking through elements which use the parent key
|
||||
local branch = {}
|
||||
while node do
|
||||
insert(branch, node)
|
||||
node = getSymbol(node.children, "UseParentKey")
|
||||
end
|
||||
return branch
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:getNodeName(node)
|
||||
if self:isFragment(node) then
|
||||
return "Fragment"
|
||||
elseif self:isPortal(node) then
|
||||
return "Portal"
|
||||
elseif self:isFunction(node) then
|
||||
return tostring(node.currentElement.component)
|
||||
elseif self:isHost(node) then
|
||||
return node.currentElement.component
|
||||
else
|
||||
return node.currentElement.component.__componentName
|
||||
end
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:getNodeIcon(node)
|
||||
if self:isFragment(node) then
|
||||
return "Fragment"
|
||||
elseif self:isPortal(node) then
|
||||
return "Portal"
|
||||
elseif self:isFunction(node) then
|
||||
return "Functional"
|
||||
elseif self:isHost(node) then
|
||||
return node.hostObject.ClassName
|
||||
else
|
||||
local componentName = node.currentElement.component.__componentName
|
||||
if componentName:find("Provider") then
|
||||
return "Provider"
|
||||
elseif componentName:find("Consumer") or componentName:find("RoduxConnection") then
|
||||
return "Consumer"
|
||||
elseif node.currentElement.component.shouldUpdate then
|
||||
return "Pure"
|
||||
else
|
||||
return "Stateful"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:getPath(instance: Instance)
|
||||
local instancePath = self:_getInstancePath(instance)
|
||||
local fullPath = self:_getFullPath(instancePath)
|
||||
return fullPath
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:_getInstancePath(instance: Instance)
|
||||
local reversePath = {}
|
||||
while instance and instance ~= self.debugInterface.rootInstance do
|
||||
-- Always prefer a number if there is a representation, as it is easier to convert back
|
||||
-- later on if need be.
|
||||
local numberName = tonumber(instance.Name)
|
||||
insert(reversePath, numberName or instance.Name)
|
||||
instance = instance.Parent
|
||||
end
|
||||
local fullPath = append({}, self.debugInterface.rootPath or self:getRootPath(), reverse(reversePath))
|
||||
if self.debugInterface.rootPrefix then
|
||||
-- Remove the prefix from the computed path
|
||||
local strippedPath = slice(fullPath, #self.debugInterface.rootPrefix + 1)
|
||||
return strippedPath
|
||||
else
|
||||
return fullPath
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns the full path including virutal nodes given a path
|
||||
-- that includes only the nodes with Roblox Instances
|
||||
function RoactTreeWatcher:_getFullPath(instancePath: Path)
|
||||
local fullPath = {}
|
||||
local root = self:getRootNode()
|
||||
local hostNode = self:getHostNode(root)
|
||||
|
||||
local found = reduce(instancePath, function(node, key: any)
|
||||
return self:_dfsFindNextChildNode(node, key, fullPath)
|
||||
end, hostNode)
|
||||
|
||||
if found ~= nil then
|
||||
return fullPath
|
||||
else
|
||||
return instancePath
|
||||
end
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:_dfsFindNextChildNode(node, key: any, fullPath: Path)
|
||||
if node == nil or node.children == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local childNode = getChildAtKey(node.children, key)
|
||||
if childNode then
|
||||
local hostNode = self:getHostNode(childNode)
|
||||
if hostNode ~= nil then
|
||||
insert(fullPath, key)
|
||||
return hostNode
|
||||
end
|
||||
end
|
||||
|
||||
for childKey, childNode in pairs(node.children) do
|
||||
local useParentKey = tostring(childKey) == "Symbol(UseParentKey)"
|
||||
if not useParentKey then
|
||||
insert(fullPath, childKey)
|
||||
end
|
||||
local found = self:_dfsFindNextChildNode(childNode, key, fullPath)
|
||||
if found ~= nil then
|
||||
return found
|
||||
end
|
||||
if not useParentKey then
|
||||
table.remove(fullPath)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function RoactTreeWatcher:destroy()
|
||||
self.tree = nil
|
||||
end
|
||||
|
||||
return RoactTreeWatcher
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export type SerializedInstance = {
|
||||
id: number;
|
||||
name: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
return {}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
local Source = script.Parent.Parent.Parent
|
||||
local Packages = Source.Parent
|
||||
local getSymbol = require(Source.RoactInspector.Utils.getSymbol)
|
||||
|
||||
local Dash = require(Packages.Dash)
|
||||
local startsWith = Dash.startsWith
|
||||
|
||||
local function getChildAtKey(object, key)
|
||||
if object == nil then
|
||||
return nil
|
||||
end
|
||||
local child = object[key]
|
||||
if child == nil and typeof(key) == "number" then
|
||||
-- Try a string representation of a numeric key if need be
|
||||
child = object[tostring(key)]
|
||||
end
|
||||
if child == nil and typeof(key) == "string" and startsWith(key, "Symbol(") then
|
||||
-- Strip Symbol() from the key
|
||||
local symbolName = key:sub(8, -2)
|
||||
child = getSymbol(object, symbolName)
|
||||
end
|
||||
return child
|
||||
end
|
||||
|
||||
return getChildAtKey
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
local foundSymbols = {}
|
||||
|
||||
local function getSymbol(object, name)
|
||||
if foundSymbols[name] then
|
||||
return object[foundSymbols[name]]
|
||||
end
|
||||
for key, value in pairs(object) do
|
||||
if tostring(key) == "Symbol(" .. name .. ")" then
|
||||
foundSymbols[name] = key
|
||||
return value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return getSymbol
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
local RoactInspectorWorker = require(script.Classes.RoactInspectorWorker)
|
||||
|
||||
return RoactInspectorWorker
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export type BridgeMessage = {
|
||||
eventName: string;
|
||||
fromBridgeId: string?;
|
||||
fromTargetId: string?;
|
||||
toBridgeId: string?;
|
||||
toTargetId: string?;
|
||||
}
|
||||
|
||||
export type BridgeListener = {
|
||||
eventName: string?;
|
||||
bridgeId: string?;
|
||||
targetId: string?;
|
||||
onEvent: (BridgeMessage) -> ();
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
--[[
|
||||
The entry point for the DeveloperTools library.
|
||||
|
||||
These static methods each create a new DebugInterface that provides a method of communication
|
||||
to the Developer Inspector plugin from the information source you are trying to inspect.
|
||||
|
||||
Each interface uses a slightly different way to communicate based on the current level of
|
||||
security and ability to use built-in services.
|
||||
]]
|
||||
local PluginDebugInterface = require(script.Classes.PluginDebugInterface)
|
||||
local CoreGuiDebugInterface = require(script.Classes.CoreGuiDebugInterface)
|
||||
local StandalonePluginDebugInterface = require(script.Classes.StandalonePluginDebugInterface)
|
||||
local LibraryDebugInterface = require(script.Classes.LibraryDebugInterface)
|
||||
local InspectorDebugInterface = require(script.Classes.InspectorDebugInterface)
|
||||
local RoactInspectorApi = require(script.RoactInspector.Classes.RoactInspectorApi)
|
||||
|
||||
local helpfulErrorMessage = "%s must be a string, did you write DeveloperTools:%s() instead of DeveloperTools.%s() by mistake?"
|
||||
|
||||
export type GuiOptions = {
|
||||
rootInstance: Instance?;
|
||||
pickerParent: Instance?;
|
||||
}
|
||||
|
||||
return {
|
||||
-- Get an inspector instance for a local, installed or built-in plugin
|
||||
forPlugin = function(pluginName: string, plugin)
|
||||
assert(typeof(pluginName) == "string", helpfulErrorMessage:format(pluginName, "forPlugin", "forPlugin"))
|
||||
assert(plugin, "DeveloperTools:forPlugin() expected plugin for argument #2")
|
||||
return PluginDebugInterface.new(pluginName, plugin)
|
||||
end,
|
||||
-- Get an inspector instance for a standalone plugin
|
||||
forStandalonePlugin = function(pluginName: string, plugin, rootInstance)
|
||||
assert(typeof(pluginName) == "string", helpfulErrorMessage:format(pluginName, "forStandalonePlugin", "forStandalonePlugin"))
|
||||
assert(plugin, "DeveloperTools:forStandalonePlugin() expected plugin for argument #2")
|
||||
return StandalonePluginDebugInterface.new(pluginName, plugin, rootInstance)
|
||||
end,
|
||||
-- Get an inspector instance for an interface placed in the CoreGui
|
||||
forCoreGui = function(appName: string, guiOptions: GuiOptions)
|
||||
assert(typeof(appName) == "string", helpfulErrorMessage:format(appName, "appName", "appName"))
|
||||
return CoreGuiDebugInterface.new(appName, guiOptions)
|
||||
end,
|
||||
-- Get an inspector instance for a library, such as UniversalApps or a set of ModuleScripts
|
||||
-- which are stored in ReplicatedStorage
|
||||
forLibrary = function(libraryName: string, guiOptions: GuiOptions)
|
||||
assert(typeof(libraryName) == "string", helpfulErrorMessage:format(libraryName, "libraryName", "libraryName"))
|
||||
return LibraryDebugInterface.new(libraryName, guiOptions)
|
||||
end,
|
||||
-- Used by the Developer Inspector plugin itself to communicate with all available channels
|
||||
forInspector = function(handlers)
|
||||
return InspectorDebugInterface.new(handlers)
|
||||
end,
|
||||
-- A reference to the RoactInspectorApi class.
|
||||
-- You can use the static isInstance method to check if the current api is a RoactInspectorApi
|
||||
-- i.e. RoactInspectorApi.isInstance(inspector:getCurrentApi())
|
||||
RoactInspectorApi = RoactInspectorApi
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
return function()
|
||||
local DeveloperTools = require(script.Parent)
|
||||
|
||||
local Dash = require(script.Parent.Parent.Dash)
|
||||
local keys = Dash.keys
|
||||
|
||||
describe("DeveloperTools", function()
|
||||
describe("Roact inspecting", function()
|
||||
it("can be initialized for a plugin", function()
|
||||
local inspector = DeveloperTools.forPlugin("TestPlugin", {})
|
||||
inspector:addRoactTree("Roact tree", {})
|
||||
expect(#keys(inspector.targets)).to.equal(1)
|
||||
end)
|
||||
it("can be initialized for a library", function()
|
||||
local inspector = DeveloperTools.forLibrary("TestLibrary", {})
|
||||
inspector:addRoactTree("Roact tree", {})
|
||||
expect(#keys(inspector.targets)).to.equal(1)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
end
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Generated by Rotriever. Format subject to change in future releases.
|
||||
name = "roblox/developer-tools"
|
||||
version = "0.1.4"
|
||||
commit = "90266d909da3b9264eb893a3761d80d97d2d3f4f"
|
||||
source = "url+https://github.com/roblox/developer-tools"
|
||||
dependencies = ["Dash roblox/dash 0.1.7 url+https://github.com/roblox/dash"]
|
||||
Reference in New Issue
Block a user