dominus-cli 0.2.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,18 +4,32 @@
4
4
  ]]
5
5
 
6
6
  local Explorer = require(script.Parent.Explorer)
7
+ local Reflection = require(script.Parent.Reflection)
7
8
 
8
9
  local UIBuilder = {}
9
10
 
11
+ -- Cache property type maps per ClassName to avoid repeated reflection calls
12
+ local propertyTypeCache = {}
13
+ local function getPropertyType(className, propName)
14
+ if not propertyTypeCache[className] then
15
+ propertyTypeCache[className] = Reflection.getPropertyTypes(className)
16
+ end
17
+ return propertyTypeCache[className][propName]
18
+ end
19
+
10
20
  local function resolveColor(value)
11
21
  if type(value) == "string" then
12
22
  if value:sub(1, 1) == "#" then
13
23
  return Color3.fromHex(value)
14
24
  end
15
25
  local r, g, b = value:match("rgb%s*%((%d+)%D+(%d+)%D+(%d+)%)")
16
- if r then return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b)) end
26
+ if r then
27
+ return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))
28
+ end
17
29
  local ok, bc = pcall(BrickColor.new, value)
18
- if ok then return bc.Color end
30
+ if ok then
31
+ return bc.Color
32
+ end
19
33
  end
20
34
  if type(value) == "table" then
21
35
  if value[1] then
@@ -35,14 +49,18 @@ local function resolveUDim2(value)
35
49
  if type(value) == "table" then
36
50
  if value.XScale or value.xScale then
37
51
  return UDim2.new(
38
- value.XScale or value.xScale or 0, value.XOffset or value.xOffset or 0,
39
- value.YScale or value.yScale or 0, value.YOffset or value.yOffset or 0
52
+ value.XScale or value.xScale or 0,
53
+ value.XOffset or value.xOffset or 0,
54
+ value.YScale or value.yScale or 0,
55
+ value.YOffset or value.yOffset or 0
40
56
  )
41
57
  end
42
58
  if value.X and type(value.X) == "table" then
43
59
  return UDim2.new(
44
- value.X.Scale or value.X[1] or 0, value.X.Offset or value.X[2] or 0,
45
- value.Y and (value.Y.Scale or value.Y[1]) or 0, value.Y and (value.Y.Offset or value.Y[2]) or 0
60
+ value.X.Scale or value.X[1] or 0,
61
+ value.X.Offset or value.X[2] or 0,
62
+ value.Y and (value.Y.Scale or value.Y[1]) or 0,
63
+ value.Y and (value.Y.Offset or value.Y[2]) or 0
46
64
  )
47
65
  end
48
66
  if value[1] ~= nil then
@@ -51,7 +69,9 @@ local function resolveUDim2(value)
51
69
  end
52
70
  if type(value) == "string" then
53
71
  local a, b, c, d = value:match("([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)")
54
- if a then return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d)) end
72
+ if a then
73
+ return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d))
74
+ end
55
75
  end
56
76
  return UDim2.new(0, 0, 0, 0)
57
77
  end
@@ -74,42 +94,187 @@ local function resolveVector2(value)
74
94
  end
75
95
 
76
96
  local function resolveEnum(instance, key, value)
77
- if type(value) ~= "string" then return value end
78
- local curOk, curVal = pcall(function() return instance[key] end)
97
+ if type(value) ~= "string" then
98
+ return value
99
+ end
100
+ local curOk, curVal = pcall(function()
101
+ return instance[key]
102
+ end)
79
103
  if curOk and typeof(curVal) == "EnumItem" then
80
104
  local enumType = tostring(curVal.EnumType)
81
- local ok, enumVal = pcall(function() return Enum[enumType][value] end)
82
- if ok then return enumVal end
105
+ local ok, enumVal = pcall(function()
106
+ return Enum[enumType][value]
107
+ end)
108
+ if ok then
109
+ return enumVal
110
+ end
83
111
  end
84
112
  return value
85
113
  end
86
114
 
87
115
  local function resolveFont(value)
88
116
  if type(value) == "string" then
89
- local ok, font = pcall(function() return Enum.Font[value] end)
90
- if ok then return font end
117
+ local ok, font = pcall(function()
118
+ return Enum.Font[value]
119
+ end)
120
+ if ok then
121
+ return font
122
+ end
91
123
  end
92
124
  return value
93
125
  end
94
126
 
95
127
  local UDIM2_PROPS = {
96
- Size = true, Position = true, AnchorPoint_UDim2 = true,
128
+ Size = true,
129
+ Position = true,
130
+ CellSize = true,
131
+ CellPadding = true,
97
132
  }
98
133
 
99
- local COLOR_PROPS = {
100
- BackgroundColor3 = true, BorderColor3 = true, TextColor3 = true,
101
- ImageColor3 = true, PlaceholderColor3 = true, TextStrokeColor3 = true,
102
- ScrollBarImageColor3 = true, Color = true,
134
+ local COLOR3_PROPS = {
135
+ BackgroundColor3 = true,
136
+ BorderColor3 = true,
137
+ TextColor3 = true,
138
+ ImageColor3 = true,
139
+ PlaceholderColor3 = true,
140
+ TextStrokeColor3 = true,
141
+ ScrollBarImageColor3 = true,
103
142
  }
104
143
 
144
+ local function resolveColorSequence(value)
145
+ if type(value) == "table" then
146
+ -- Array of keypoints: {{time, color}, ...}
147
+ if type(value[1]) == "table" then
148
+ local keypoints = {}
149
+ for _, kp in ipairs(value) do
150
+ local t = kp[1] or kp.Time or 0
151
+ local c = resolveColor(kp[2] or kp.Color or kp)
152
+ table.insert(keypoints, ColorSequenceKeypoint.new(t, c))
153
+ end
154
+ return ColorSequence.new(keypoints)
155
+ end
156
+ -- Single color as array [r,g,b] → uniform ColorSequence
157
+ if type(value[1]) == "number" and #value == 3 then
158
+ local c = resolveColor(value)
159
+ return ColorSequence.new(c)
160
+ end
161
+ end
162
+ -- String hex/rgb → uniform ColorSequence
163
+ if type(value) == "string" then
164
+ return ColorSequence.new(resolveColor(value))
165
+ end
166
+ return ColorSequence.new(Color3.new(1, 1, 1))
167
+ end
168
+
169
+ local function resolveNumberSequence(value)
170
+ if type(value) == "table" then
171
+ -- Array of keypoints: {{time, value, envelope?}, ...}
172
+ if type(value[1]) == "table" then
173
+ local keypoints = {}
174
+ for _, kp in ipairs(value) do
175
+ local t = kp[1] or kp.Time or 0
176
+ local v = kp[2] or kp.Value or 0
177
+ local e = kp[3] or kp.Envelope or 0
178
+ table.insert(keypoints, NumberSequenceKeypoint.new(t, v, e))
179
+ end
180
+ return NumberSequence.new(keypoints)
181
+ end
182
+ end
183
+ if type(value) == "number" then
184
+ return NumberSequence.new(value)
185
+ end
186
+ return NumberSequence.new(0)
187
+ end
188
+
105
189
  local VECTOR2_PROPS = {
106
- AnchorPoint = true, CanvasSize_v2 = true,
190
+ AnchorPoint = true,
191
+ CanvasSize_v2 = true,
192
+ AbsoluteSize = true,
193
+ AbsolutePosition = true,
194
+ }
195
+
196
+ local UDIM_PROPS = {
197
+ CornerRadius = true,
198
+ Padding = true,
199
+ PaddingTop = true,
200
+ PaddingBottom = true,
201
+ PaddingLeft = true,
202
+ PaddingRight = true,
203
+ FillDirection = false, -- not UDim
107
204
  }
108
205
 
206
+ local NUMBER_RANGE_PROPS = {
207
+ Range = true,
208
+ RotationRange = true,
209
+ Size_NR = true, -- particle size range alias
210
+ }
211
+
212
+ local RECT_PROPS = {
213
+ SliceCenter = true,
214
+ ImageRectOffset_Rect = true,
215
+ }
216
+
217
+ local function resolveNumberRange(value)
218
+ if type(value) == "table" then
219
+ return NumberRange.new(value[1] or 0, value[2] or value[1] or 0)
220
+ end
221
+ if type(value) == "number" then
222
+ return NumberRange.new(value)
223
+ end
224
+ return NumberRange.new(0)
225
+ end
226
+
227
+ local function resolveRect(value)
228
+ if type(value) == "table" then
229
+ if #value == 4 then
230
+ return Rect.new(value[1], value[2], value[3], value[4])
231
+ end
232
+ return Rect.new(
233
+ value.Min and value.Min[1] or value.MinX or 0,
234
+ value.Min and value.Min[2] or value.MinY or 0,
235
+ value.Max and value.Max[1] or value.MaxX or 0,
236
+ value.Max and value.Max[2] or value.MaxY or 0
237
+ )
238
+ end
239
+ return Rect.new(0, 0, 0, 0)
240
+ end
241
+
242
+ -- Disambiguate 2-element arrays: check actual property type on the instance
243
+ local function resolve2ElementArray(instance, key, value)
244
+ -- First, check our known lookup tables
245
+ if VECTOR2_PROPS[key] then
246
+ return resolveVector2(value)
247
+ end
248
+ if UDIM_PROPS[key] then
249
+ return resolveUDim(value)
250
+ end
251
+ if NUMBER_RANGE_PROPS[key] then
252
+ return resolveNumberRange(value)
253
+ end
254
+ -- Fall back to runtime type introspection
255
+ local ok, curVal = pcall(function()
256
+ return instance[key]
257
+ end)
258
+ if ok and curVal ~= nil then
259
+ local t = typeof(curVal)
260
+ if t == "Vector2" then
261
+ return resolveVector2(value)
262
+ end
263
+ if t == "UDim" then
264
+ return resolveUDim(value)
265
+ end
266
+ if t == "NumberRange" then
267
+ return resolveNumberRange(value)
268
+ end
269
+ end
270
+ -- Default: assume UDim for 2-element (more common in UI)
271
+ return resolveUDim(value)
272
+ end
273
+
109
274
  local function applyProperty(instance, key, value)
110
275
  -- Special handling by property name
111
276
  if key == "Size" or key == "Position" then
112
- if instance:IsA("GuiObject") or instance:IsA("ScreenGui") then
277
+ if instance:IsA("GuiObject") or instance:IsA("UIBase") then
113
278
  instance[key] = resolveUDim2(value)
114
279
  return
115
280
  end
@@ -127,16 +292,63 @@ local function applyProperty(instance, key, value)
127
292
  return
128
293
  end
129
294
 
130
- if key == "CornerRadius" then
295
+ if UDIM_PROPS[key] then
131
296
  instance[key] = resolveUDim(value)
132
297
  return
133
298
  end
134
299
 
135
- if COLOR_PROPS[key] then
300
+ -- Use reflection to distinguish Color3 vs ColorSequence, NumberSequence, etc.
301
+ local propType = getPropertyType(instance.ClassName, key)
302
+
303
+ if propType == "ColorSequence" then
304
+ instance[key] = resolveColorSequence(value)
305
+ return
306
+ end
307
+
308
+ if propType == "NumberSequence" then
309
+ instance[key] = resolveNumberSequence(value)
310
+ return
311
+ end
312
+
313
+ if COLOR3_PROPS[key] or propType == "Color3" then
136
314
  instance[key] = resolveColor(value)
137
315
  return
138
316
  end
139
317
 
318
+ -- Reflection-based coercion for types not in hardcoded tables
319
+ if propType == "UDim2" and type(value) == "table" then
320
+ instance[key] = resolveUDim2(value)
321
+ return
322
+ end
323
+ if propType == "UDim" then
324
+ instance[key] = resolveUDim(value)
325
+ return
326
+ end
327
+ if propType == "Vector2" then
328
+ instance[key] = resolveVector2(value)
329
+ return
330
+ end
331
+ if propType == "NumberRange" then
332
+ instance[key] = resolveNumberRange(value)
333
+ return
334
+ end
335
+ if propType == "Rect" then
336
+ instance[key] = resolveRect(value)
337
+ return
338
+ end
339
+
340
+ -- Rect properties (4-element arrays like SliceCenter)
341
+ if RECT_PROPS[key] then
342
+ instance[key] = resolveRect(value)
343
+ return
344
+ end
345
+
346
+ -- NumberRange properties
347
+ if NUMBER_RANGE_PROPS[key] then
348
+ instance[key] = resolveNumberRange(value)
349
+ return
350
+ end
351
+
140
352
  if key == "Font" and instance:IsA("TextLabel") or instance:IsA("TextButton") or instance:IsA("TextBox") then
141
353
  instance[key] = resolveFont(value)
142
354
  return
@@ -147,8 +359,12 @@ local function applyProperty(instance, key, value)
147
359
  local family = value.Family or "rbxasset://fonts/families/SourceSansPro.json"
148
360
  local weight = Enum.FontWeight.Regular
149
361
  local style = Enum.FontStyle.Normal
150
- pcall(function() weight = Enum.FontWeight[value.Weight or "Regular"] end)
151
- pcall(function() style = Enum.FontStyle[value.Style or "Normal"] end)
362
+ pcall(function()
363
+ weight = Enum.FontWeight[value.Weight or "Regular"]
364
+ end)
365
+ pcall(function()
366
+ style = Enum.FontStyle[value.Style or "Normal"]
367
+ end)
152
368
  instance.FontFace = Font.new(family, weight, style)
153
369
  end
154
370
  return
@@ -157,19 +373,68 @@ local function applyProperty(instance, key, value)
157
373
  -- Enum properties (string values)
158
374
  if type(value) == "string" then
159
375
  local resolved = resolveEnum(instance, key, value)
160
- local ok, err = pcall(function() instance[key] = resolved end)
376
+ local ok, err = pcall(function()
377
+ instance[key] = resolved
378
+ end)
161
379
  if not ok then
162
380
  -- Maybe it's a direct value
163
- pcall(function() instance[key] = value end)
381
+ pcall(function()
382
+ instance[key] = value
383
+ end)
164
384
  end
165
385
  return
166
386
  end
167
387
 
388
+ if UDIM2_PROPS[key] then
389
+ pcall(function()
390
+ instance[key] = resolveUDim2(value)
391
+ end)
392
+ return
393
+ end
394
+
395
+ -- Smart disambiguation for 2-element table arrays
396
+ if type(value) == "table" and #value == 2 then
397
+ local resolved = resolve2ElementArray(instance, key, value)
398
+ pcall(function()
399
+ instance[key] = resolved
400
+ end)
401
+ return
402
+ end
403
+
404
+ -- 4-element table arrays: try Rect if not already handled as UDim2
405
+ if type(value) == "table" and #value == 4 then
406
+ -- Check actual property type
407
+ local ok, curVal = pcall(function()
408
+ return instance[key]
409
+ end)
410
+ if ok and curVal ~= nil then
411
+ local t = typeof(curVal)
412
+ if t == "Rect" then
413
+ pcall(function()
414
+ instance[key] = resolveRect(value)
415
+ end)
416
+ return
417
+ elseif t == "UDim2" then
418
+ pcall(function()
419
+ instance[key] = resolveUDim2(value)
420
+ end)
421
+ return
422
+ end
423
+ end
424
+ -- Default 4-element to UDim2
425
+ pcall(function()
426
+ instance[key] = resolveUDim2(value)
427
+ end)
428
+ return
429
+ end
430
+
168
431
  -- Direct assignment for numbers, booleans, etc.
169
- pcall(function() instance[key] = value end)
432
+ pcall(function()
433
+ instance[key] = value
434
+ end)
170
435
  end
171
436
 
172
- local function buildNode(spec, parent)
437
+ local function buildNode(spec, parent, animate)
173
438
  local className = spec.ClassName or spec.className or "Frame"
174
439
  local name = spec.Name or spec.name or className
175
440
 
@@ -178,7 +443,14 @@ local function buildNode(spec, parent)
178
443
 
179
444
  -- Apply all properties
180
445
  for key, value in spec do
181
- if key ~= "ClassName" and key ~= "className" and key ~= "Name" and key ~= "name" and key ~= "Children" and key ~= "children" then
446
+ if
447
+ key ~= "ClassName"
448
+ and key ~= "className"
449
+ and key ~= "Name"
450
+ and key ~= "name"
451
+ and key ~= "Children"
452
+ and key ~= "children"
453
+ then
182
454
  applyProperty(instance, key, value)
183
455
  end
184
456
  end
@@ -190,7 +462,10 @@ local function buildNode(spec, parent)
190
462
  local children = spec.Children or spec.children
191
463
  if children then
192
464
  for _, childSpec in children do
193
- buildNode(childSpec, instance)
465
+ if animate then
466
+ task.wait(0.05)
467
+ end
468
+ buildNode(childSpec, instance, animate)
194
469
  end
195
470
  end
196
471
 
@@ -203,7 +478,9 @@ function UIBuilder.build(spec)
203
478
  if not parent then
204
479
  -- Try as a service
205
480
  local ok
206
- ok, parent = pcall(function() return game:GetService(parentPath) end)
481
+ ok, parent = pcall(function()
482
+ return game:GetService(parentPath)
483
+ end)
207
484
  if not ok then
208
485
  return { success = false, error = "Parent not found: " .. parentPath }
209
486
  end
@@ -223,8 +500,13 @@ function UIBuilder.build(spec)
223
500
  return { success = false, error = "No 'tree' provided in spec" }
224
501
  end
225
502
 
503
+ local animate = false
504
+ if spec.animate ~= nil then
505
+ animate = spec.animate
506
+ end
507
+
226
508
  local ok, result = pcall(function()
227
- return buildNode(tree, parent)
509
+ return buildNode(tree, parent, animate)
228
510
  end)
229
511
 
230
512
  if not ok then
@@ -244,4 +526,184 @@ function UIBuilder.build(spec)
244
526
  return { success = true, path = path, instanceCount = count }
245
527
  end
246
528
 
529
+ --[[
530
+ Serialize: convert an existing instance tree into create_ui-compatible JSON.
531
+ Reads properties via ReflectionService, converts Roblox types to JSON-friendly values,
532
+ and skips default/unchanged values to keep output minimal.
533
+ ]]
534
+
535
+ local Reflection = require(script.Parent.Reflection)
536
+ local ReflectionService = game:GetService("ReflectionService")
537
+
538
+ -- Convert a Roblox value to a JSON-friendly representation
539
+ local function valueToJson(value, propType)
540
+ local t = typeof(value)
541
+
542
+ if t == "UDim2" then
543
+ return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
544
+ end
545
+ if t == "UDim" then
546
+ return { value.Scale, value.Offset }
547
+ end
548
+ if t == "Vector2" then
549
+ return { value.X, value.Y }
550
+ end
551
+ if t == "Vector3" then
552
+ return { value.X, value.Y, value.Z }
553
+ end
554
+ if t == "Color3" then
555
+ return "#" .. value:ToHex()
556
+ end
557
+ if t == "BrickColor" then
558
+ return value.Name
559
+ end
560
+ if t == "ColorSequence" then
561
+ local kps = {}
562
+ for _, kp in value.Keypoints do
563
+ table.insert(kps, { kp.Time, "#" .. kp.Value:ToHex() })
564
+ end
565
+ return kps
566
+ end
567
+ if t == "NumberSequence" then
568
+ local kps = {}
569
+ for _, kp in value.Keypoints do
570
+ table.insert(kps, { kp.Time, kp.Value, kp.Envelope })
571
+ end
572
+ return kps
573
+ end
574
+ if t == "NumberRange" then
575
+ return { value.Min, value.Max }
576
+ end
577
+ if t == "Rect" then
578
+ return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y }
579
+ end
580
+ if t == "CFrame" then
581
+ return { value:GetComponents() }
582
+ end
583
+ if t == "EnumItem" then
584
+ return value.Name
585
+ end
586
+ if t == "Font" then
587
+ return {
588
+ Family = value.Family,
589
+ Weight = value.Weight.Name,
590
+ Style = value.Style.Name,
591
+ }
592
+ end
593
+ if t == "boolean" or t == "number" or t == "string" then
594
+ return value
595
+ end
596
+ -- Fallback: tostring
597
+ return tostring(value)
598
+ end
599
+
600
+ local SERIALIZE_SKIP = {
601
+ Parent = true,
602
+ ClassName = true,
603
+ Name = true,
604
+ Archivable = true,
605
+ AbsolutePosition = true,
606
+ AbsoluteSize = true,
607
+ AbsoluteRotation = true,
608
+ IsLoaded = true,
609
+ }
610
+
611
+ local function serializeNode(instance, maxDepth, depth)
612
+ if depth > maxDepth then
613
+ return nil
614
+ end
615
+
616
+ local node = {
617
+ ClassName = instance.ClassName,
618
+ Name = instance.Name,
619
+ }
620
+
621
+ -- Get non-default properties via reflection
622
+ local ok, propData = pcall(function()
623
+ return ReflectionService:GetPropertiesOfClass(instance.ClassName)
624
+ end)
625
+
626
+ if ok and propData then
627
+ -- Create a default instance to compare against
628
+ local defaultInstance = nil
629
+ pcall(function()
630
+ defaultInstance = Instance.new(instance.ClassName)
631
+ end)
632
+
633
+ for _, prop in propData do
634
+ if not SERIALIZE_SKIP[prop.Name] and not prop.Name:match("^Absolute") then
635
+ local valOk, value = pcall(function()
636
+ return instance[prop.Name]
637
+ end)
638
+ if valOk and value ~= nil then
639
+ -- Skip default values
640
+ local isDefault = false
641
+ if defaultInstance then
642
+ local defOk, defVal = pcall(function()
643
+ return defaultInstance[prop.Name]
644
+ end)
645
+ if defOk and defVal == value then
646
+ isDefault = true
647
+ end
648
+ end
649
+
650
+ if not isDefault then
651
+ local jsonVal = valueToJson(value, prop.ValueType and tostring(prop.ValueType))
652
+ if jsonVal ~= nil then
653
+ node[prop.Name] = jsonVal
654
+ end
655
+ end
656
+ end
657
+ end
658
+ end
659
+
660
+ if defaultInstance then
661
+ pcall(function()
662
+ defaultInstance:Destroy()
663
+ end)
664
+ end
665
+ end
666
+
667
+ -- Serialize children
668
+ local children = instance:GetChildren()
669
+ if #children > 0 then
670
+ node.Children = {}
671
+ for _, child in children do
672
+ local childNode = serializeNode(child, maxDepth, depth + 1)
673
+ if childNode then
674
+ table.insert(node.Children, childNode)
675
+ end
676
+ end
677
+ if #node.Children == 0 then
678
+ node.Children = nil
679
+ end
680
+ end
681
+
682
+ return node
683
+ end
684
+
685
+ function UIBuilder.serialize(spec)
686
+ local path = spec.path
687
+ if not path then
688
+ return { success = false, error = "Missing 'path' parameter" }
689
+ end
690
+
691
+ local instance = Explorer.resolveInstance(path)
692
+ if not instance then
693
+ return { success = false, error = "Instance not found: " .. path }
694
+ end
695
+
696
+ local maxDepth = spec.maxDepth or 50
697
+
698
+ local ok, tree = pcall(function()
699
+ return serializeNode(instance, maxDepth, 0)
700
+ end)
701
+
702
+ if not ok then
703
+ return { success = false, error = "Serialization failed: " .. tostring(tree) }
704
+ end
705
+
706
+ return { success = true, tree = tree }
707
+ end
708
+
247
709
  return UIBuilder