This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
export type SerializedInstance = {
id: number;
name: string;
className: string;
}
return {}
@@ -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
@@ -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
@@ -0,0 +1,3 @@
local RoactInspectorWorker = require(script.Classes.RoactInspectorWorker)
return RoactInspectorWorker
@@ -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) -> ();
}
@@ -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
}
@@ -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
@@ -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"]