@rbxts/app-forge 0.7.1 → 0.7.2-alpha.2

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.
@@ -0,0 +1,8 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local Debugger = TS.import(script, script, "debugger").default
4
+ local Logger = TS.import(script, script, "logger").default
5
+ return {
6
+ Debugger = Debugger,
7
+ Logger = Logger,
8
+ }
@@ -0,0 +1,5 @@
1
+ export default class Logger {
2
+ private readonly scope;
3
+ constructor(scope: string);
4
+ log(level: "DEBUG" | "PERF" | "INFO" | "WARN" | "ERROR", message: string, data?: unknown, traceback?: string): void;
5
+ }
@@ -0,0 +1,37 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ -- debug/logger.ts
4
+ local RunService = TS.import(script, TS.getModule(script, "@rbxts", "services")).RunService
5
+ local Logger
6
+ do
7
+ Logger = setmetatable({}, {
8
+ __tostring = function()
9
+ return "Logger"
10
+ end,
11
+ })
12
+ Logger.__index = Logger
13
+ function Logger.new(...)
14
+ local self = setmetatable({}, Logger)
15
+ return self:constructor(...) or self
16
+ end
17
+ function Logger:constructor(scope)
18
+ self.scope = scope
19
+ end
20
+ function Logger:log(level, message, data, traceback)
21
+ if not RunService:IsStudio() then
22
+ return nil
23
+ end
24
+ local prefix = `[{self.scope}][{level}]`
25
+ if data ~= nil then
26
+ print(prefix, message, data)
27
+ else
28
+ print(prefix, message)
29
+ end
30
+ if traceback ~= "" and traceback then
31
+ print(traceback)
32
+ end
33
+ end
34
+ end
35
+ return {
36
+ default = Logger,
37
+ }
@@ -2,7 +2,15 @@ import Vide from "@rbxts/vide";
2
2
  import type Types from "./types";
3
3
  import type AppForge from ".";
4
4
  export declare const AppRegistry: Map<string, Types.AppRegistry.Static>;
5
+ /**
6
+ * Registers a Vide App with AppForge.
7
+ *
8
+ * This runs at definition time and validates static configuration.
9
+ */
5
10
  export declare function App<N extends AppNames>(props: Types.AppRegistry.Props<N>): <T extends new (props: Types.Props.Main, name: AppNames) => Args>(constructor: T) => T;
11
+ /**
12
+ * Base class for all AppForge Apps.
13
+ */
6
14
  export declare abstract class Args {
7
15
  readonly forge: AppForge;
8
16
  readonly props: Types.Props.Class;
@@ -1,15 +1,40 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local TS = _G[script]
3
3
  -- Packages
4
- local px = TS.import(script, TS.getModule(script, "@rbxts", "loners-pretty-vide-utils").out).px
5
4
  -- Types
5
+ -- Hooks
6
+ local px = TS.import(script, script.Parent, "hooks", "usePx").px
7
+ -- Debug
8
+ local Logger = TS.import(script, script.Parent, "debug", "logger").default
9
+ local logger = Logger.new("AppRegistry")
6
10
  local AppRegistry = {}
11
+ --[[
12
+ *
13
+ * Registers a Vide App with AppForge.
14
+ *
15
+ * This runs at definition time and validates static configuration.
16
+
17
+ ]]
7
18
  local function App(props)
8
19
  return function(constructor)
9
20
  local _name = props.name
10
21
  if AppRegistry[_name] ~= nil then
11
- error(`Duplicate registered App name "{props.name}"`)
22
+ logger:log("ERROR", "Duplicate App name detected", {
23
+ name = props.name,
24
+ })
25
+ error(`Duplicate registered App name "{props.name}". ` .. `App names must be globally unique.`, 2)
12
26
  end
27
+ local _value = props.name
28
+ if not (_value ~= "" and _value) then
29
+ logger:log("ERROR", "Attempted to register App without a name", props)
30
+ error("App registration failed: missing app name", 2)
31
+ end
32
+ logger:log("DEBUG", "Registering App", {
33
+ name = props.name,
34
+ renderGroup = props.renderGroup,
35
+ visible = props.visible,
36
+ rules = props.rules,
37
+ })
13
38
  local _name_1 = props.name
14
39
  local _arg1 = {
15
40
  constructor = constructor,
@@ -21,6 +46,11 @@ local function App(props)
21
46
  return constructor
22
47
  end
23
48
  end
49
+ --[[
50
+ *
51
+ * Base class for all AppForge Apps.
52
+
53
+ ]]
24
54
  local Args
25
55
  do
26
56
  Args = {}
@@ -28,13 +58,20 @@ do
28
58
  local _binding = props
29
59
  local forge = _binding.forge
30
60
  self.forge = forge
61
+ self.name = name
31
62
  local _object = table.clone(props.props)
32
63
  setmetatable(_object, nil)
33
64
  _object.px = px
34
65
  _object.forge = forge
35
66
  self.props = _object
36
- self.name = name
37
- self.source = forge:getSource(name)
67
+ local src = forge:getSource(name)
68
+ if not src then
69
+ logger:log("ERROR", "Missing visibility source for App", {
70
+ name = name,
71
+ })
72
+ error(`Failed to retrieve visibility source for app "{name}"`, 2)
73
+ end
74
+ self.source = src
38
75
  end
39
76
  end
40
77
  return {
@@ -1,5 +1,5 @@
1
1
  declare const _default: () => {
2
2
  forge: import("..").default;
3
- px: typeof import("@rbxts/loners-pretty-vide-utils").px;
3
+ px: typeof import("./usePx").px;
4
4
  };
5
5
  export default _default;
@@ -2,10 +2,14 @@
2
2
  local TS = _G[script]
3
3
  -- Components
4
4
  local Contexts = TS.import(script, script.Parent.Parent, "context").default
5
+ -- Debug
6
+ local Logger = TS.import(script, script.Parent.Parent, "debug", "logger").default
7
+ local logger = Logger.new("useAppContext")
5
8
  local default = function()
6
9
  local context = Contexts.App()
7
10
  if not context then
8
- error(`Failed to retrieve App Props for Vide {debug.traceback()}`)
11
+ logger:log("ERROR", "Failed to retrieve App context")
12
+ error(`Failed to retrieve App Props for Vide\n{debug.traceback()}`, 2)
9
13
  end
10
14
  return context
11
15
  end
@@ -0,0 +1,17 @@
1
+ type EventLike<T extends Callback = Callback> = {
2
+ Connect(callback: T): ConnectionLike;
3
+ } | {
4
+ connect(callback: T): ConnectionLike;
5
+ } | {
6
+ subscribe(callback: T): ConnectionLike;
7
+ };
8
+ type ConnectionLike = {
9
+ Disconnect(): void;
10
+ } | {
11
+ disconnect(): void;
12
+ } | (() => void);
13
+ /**
14
+ * Subscribes to an event-like object and auto-cleans up.
15
+ */
16
+ export declare function useEventListener<T extends EventLike>(event: T, listener: T extends EventLike<infer U> ? U : never): ReturnType<T>;
17
+ export {};
@@ -0,0 +1,61 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local cleanup = TS.import(script, TS.getModule(script, "@rbxts", "vide").src).cleanup
4
+ -- Debug
5
+ local Logger = TS.import(script, script.Parent.Parent, "debug", "logger").default
6
+ local logger = Logger.new("useEventListener")
7
+ local connect = function(event, callback)
8
+ local _event = event
9
+ if typeof(_event) == "RBXScriptSignal" then
10
+ local connection
11
+ connection = event:Connect(function(...)
12
+ local args = { ... }
13
+ if connection.Connected then
14
+ return callback(unpack(args))
15
+ end
16
+ end)
17
+ return connection
18
+ elseif event.Connect ~= nil then
19
+ return event:Connect(callback)
20
+ elseif event.connect ~= nil then
21
+ return event:connect(callback)
22
+ elseif event.subscribe ~= nil then
23
+ return event:subscribe(callback)
24
+ end
25
+ logger:log("ERROR", "Unsupported event-like object", event)
26
+ error("Event-like object does not have a supported connect method.", 2)
27
+ end
28
+ local disconnect = function(connection)
29
+ local _connection = connection
30
+ if type(_connection) == "function" then
31
+ connection()
32
+ else
33
+ local _connection_1 = connection
34
+ local _condition = typeof(_connection_1) == "RBXScriptConnection"
35
+ if not _condition then
36
+ _condition = connection.Disconnect ~= nil
37
+ end
38
+ if _condition then
39
+ connection:Disconnect()
40
+ elseif connection.disconnect ~= nil then
41
+ connection:disconnect()
42
+ else
43
+ logger:log("WARN", "Unsupported connection-like object during cleanup", connection)
44
+ end
45
+ end
46
+ end
47
+ --[[
48
+ *
49
+ * Subscribes to an event-like object and auto-cleans up.
50
+
51
+ ]]
52
+ local function useEventListener(event, listener)
53
+ local connection = connect(event, listener)
54
+ cleanup(function()
55
+ return disconnect(connection)
56
+ end)
57
+ return connection
58
+ end
59
+ return {
60
+ useEventListener = useEventListener,
61
+ }
@@ -0,0 +1,11 @@
1
+ export declare const px: ((value: number) => number) & {
2
+ scale: (value: number) => number;
3
+ even: (value: number) => number;
4
+ floor: (value: number) => number;
5
+ ceil: (value: number) => number;
6
+ };
7
+ /**
8
+ * Initializes global px scaling.
9
+ * Must be called exactly once.
10
+ */
11
+ export declare function usePx(target?: GuiObject | Camera, baseResolution?: Vector2, minScale?: number): void;
@@ -0,0 +1,103 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ -- Services
4
+ local Workspace = TS.import(script, TS.getModule(script, "@rbxts", "services")).Workspace
5
+ -- Packages
6
+ local source = TS.import(script, TS.getModule(script, "@rbxts", "vide").src).source
7
+ -- Helpers
8
+ local useEventListener = TS.import(script, script.Parent, "useEventListener").useEventListener
9
+ -- Debug
10
+ local Logger = TS.import(script, script.Parent.Parent, "debug", "logger").default
11
+ local logger = Logger.new("usePx")
12
+ --* Default reference resolution for px calculations
13
+ local BASE_RESOLUTION = source(Vector2.new(1920, 1080))
14
+ --* Minimum allowed scale to prevent unreadable UI
15
+ local MIN_SCALE = source(0.5)
16
+ --* 0 = width-based, 1 = height-based
17
+ local DOMINANT_AXIS = 0.5
18
+ local TARGET = source(Workspace.CurrentCamera)
19
+ local SCALE = source(1)
20
+ local INITIALIZED = false
21
+ local function callable(callback, object)
22
+ return setmetatable(object, {
23
+ __call = function(_, ...)
24
+ local args = { ... }
25
+ return callback(unpack(args))
26
+ end,
27
+ })
28
+ end
29
+ local px = callable(function(value)
30
+ return math.round(value * SCALE())
31
+ end, {
32
+ scale = function(value)
33
+ return value * SCALE()
34
+ end,
35
+ even = function(value)
36
+ return math.round(value * SCALE() * 0.5) * 2
37
+ end,
38
+ floor = function(value)
39
+ return math.floor(value * SCALE())
40
+ end,
41
+ ceil = function(value)
42
+ return math.ceil(value * SCALE())
43
+ end,
44
+ })
45
+ local function calculateScale()
46
+ local target = TARGET()
47
+ if not target then
48
+ return nil
49
+ end
50
+ local size = if target:IsA("Camera") then target.ViewportSize elseif target:IsA("GuiObject") then target.AbsoluteSize else nil
51
+ if not size then
52
+ return nil
53
+ end
54
+ local res = BASE_RESOLUTION()
55
+ if res.X <= 0 or res.Y <= 0 then
56
+ return nil
57
+ end
58
+ local min = MIN_SCALE()
59
+ local width = math.log(size.X / res.X, 2)
60
+ local height = math.log(size.Y / res.Y, 2)
61
+ local centered = width + (height - width) * DOMINANT_AXIS
62
+ local scale = 2 ^ centered
63
+ SCALE(math.max(scale, min))
64
+ end
65
+ --[[
66
+ *
67
+ * Initializes global px scaling.
68
+ * Must be called exactly once.
69
+
70
+ ]]
71
+ local function usePx(target, baseResolution, minScale)
72
+ if INITIALIZED then
73
+ logger:log("WARN", "usePx() called more than once")
74
+ return nil
75
+ end
76
+ INITIALIZED = true
77
+ logger:log("DEBUG", "Initializing px scaling", {
78
+ target = target,
79
+ baseResolution = baseResolution,
80
+ minScale = minScale,
81
+ })
82
+ if baseResolution then
83
+ BASE_RESOLUTION(baseResolution)
84
+ end
85
+ if minScale ~= nil then
86
+ MIN_SCALE(minScale)
87
+ end
88
+ if target then
89
+ TARGET(target)
90
+ end
91
+ local resolvedTarget = TARGET()
92
+ if not resolvedTarget then
93
+ logger:log("WARN", "usePx(): no valid target to observe")
94
+ return nil
95
+ end
96
+ local signal = if resolvedTarget:IsA("Camera") then resolvedTarget:GetPropertyChangedSignal("ViewportSize") else resolvedTarget:GetPropertyChangedSignal("AbsoluteSize")
97
+ useEventListener(signal, calculateScale)
98
+ calculateScale()
99
+ end
100
+ return {
101
+ usePx = usePx,
102
+ px = px,
103
+ }
@@ -1,6 +1,7 @@
1
1
  import Vide from "@rbxts/vide";
2
2
  import Renders from "./classes/renders";
3
3
  import Types from "./types";
4
+ import { Logger, Debugger } from "./debug";
4
5
  type Destructor = () => void;
5
6
  type Loaded = {
6
7
  container: Vide.Node;
@@ -8,17 +9,19 @@ type Loaded = {
8
9
  anchor?: Vide.Node;
9
10
  };
10
11
  export default class AppForge extends Renders {
12
+ readonly logger: Logger;
13
+ readonly debug: Debugger;
11
14
  protected sources: Map<string, Vide.Source<boolean>>;
12
15
  protected loaded: Map<string, Loaded>;
13
16
  protected innerMount?: Destructor;
14
17
  protected __px: boolean;
15
18
  constructor();
16
- protected createSource(name: AppNames): typeof Vide.source | undefined;
19
+ protected createSource(name: AppNames): void;
20
+ getSource(name: AppNames): Vide.Source<boolean>;
17
21
  isLoaded(name: AppNames): boolean;
18
22
  bind(name: AppNames, value: Vide.Source<boolean>): void;
19
23
  anchor(name: AppNames, anchorName: AppNames, props: Types.Props.Main): void;
20
24
  index(name: AppNames, index: number): void;
21
- getSource(name: AppNames): Vide.Source<boolean>;
22
25
  set(name: AppNames, value: boolean, rules?: boolean): void;
23
26
  open(name: AppNames, rules?: boolean): void;
24
27
  close(name: AppNames, rules?: boolean): void;
@@ -14,6 +14,10 @@ local untrack = _vide.untrack
14
14
  local Renders = TS.import(script, script, "classes", "renders").default
15
15
  -- Helpers
16
16
  local AppRegistry = TS.import(script, script, "decorator").AppRegistry
17
+ -- Debug
18
+ local _debug = TS.import(script, script, "debug")
19
+ local Logger = _debug.Logger
20
+ local Debugger = _debug.Debugger
17
21
  local AppForge
18
22
  do
19
23
  local super = Renders
@@ -30,6 +34,10 @@ do
30
34
  end
31
35
  function AppForge:constructor()
32
36
  super.constructor(self)
37
+ self.logger = Logger.new("AppForge")
38
+ self.debug = Debugger.new(function(level, msg, data, trace)
39
+ return self.logger:log(level, msg, data, trace)
40
+ end)
33
41
  self.sources = {}
34
42
  self.loaded = {}
35
43
  self.__px = false
@@ -46,22 +54,48 @@ do
46
54
  local _name = name
47
55
  local app = AppRegistry[_name]
48
56
  if not app then
49
- error(`App "{name}" not registered`)
57
+ self.logger:log("ERROR", "App not registered while creating source", {
58
+ name = name,
59
+ })
60
+ return nil
50
61
  end
51
62
  local _sources = self.sources
52
63
  local _name_1 = name
53
64
  if _sources[_name_1] ~= nil then
54
65
  return nil
55
66
  end
56
- local _sources_1 = self.sources
67
+ local _debug_1 = self.debug
57
68
  local _exp = name
69
+ local _object = {}
70
+ local _left = "default"
58
71
  local _condition = app.visible
59
72
  if _condition == nil then
60
73
  _condition = false
61
74
  end
62
- local _arg1 = source(_condition)
63
- _sources_1[_exp] = _arg1
64
- return source
75
+ _object[_left] = _condition
76
+ _debug_1:logTag("state", _exp, "Creating visibility source", _object)
77
+ local _sources_1 = self.sources
78
+ local _exp_1 = name
79
+ local _condition_1 = app.visible
80
+ if _condition_1 == nil then
81
+ _condition_1 = false
82
+ end
83
+ local _arg1 = source(_condition_1)
84
+ _sources_1[_exp_1] = _arg1
85
+ end
86
+ function AppForge:getSource(name)
87
+ local _sources = self.sources
88
+ local _name = name
89
+ if not (_sources[_name] ~= nil) then
90
+ self:createSource(name)
91
+ end
92
+ local _sources_1 = self.sources
93
+ local _name_1 = name
94
+ local src = _sources_1[_name_1]
95
+ if not src then
96
+ error(`AppForge invariant broken: missing visibility source for {name}`, 2)
97
+ end
98
+ return src
65
99
  end
66
100
  function AppForge:isLoaded(name)
67
101
  local _loaded = self.loaded
@@ -70,6 +104,7 @@ do
70
104
  end
71
105
  function AppForge:bind(name, value)
72
106
  if not RunService:IsRunning() then
107
+ self.debug:logTag("state", name, "Binding external visibility source")
73
108
  local _sources = self.sources
74
109
  local _name = name
75
110
  local _value = value
@@ -81,17 +116,26 @@ do
81
116
  end)
82
117
  end)
83
118
  else
84
- warn("forge.bind is used for studio when game isnt running")
119
+ self.logger:log("WARN", "forge.bind called while game is running", {
120
+ name = name,
121
+ })
85
122
  end
86
123
  end
87
124
  function AppForge:anchor(name, anchorName, props)
88
125
  if name == anchorName then
89
- error(`Tried to anchor an App to itself`)
126
+ self.logger:log("ERROR", "Attempted to anchor app to itself", {
127
+ name = name,
128
+ })
129
+ return nil
90
130
  end
91
131
  local _anchorName = anchorName
92
132
  local anchorApp = AppRegistry[_anchorName]
93
133
  if not anchorApp then
94
- error(`Failed to get class for {anchorName} from AppRegistry for anchor`)
134
+ self.logger:log("ERROR", "Anchor parent not registered", {
135
+ child = name,
136
+ parent = anchorName,
137
+ })
138
+ return nil
95
139
  end
96
140
  local _loaded = self.loaded
97
141
  local _name = name
@@ -101,11 +145,17 @@ do
101
145
  end
102
146
  local render = _render
103
147
  if not render then
104
- error(`Failed to get {name} from this.loaded for anchor to {anchorName}`)
148
+ self.debug:logTag("rules", name, "Anchor skipped (child not rendered yet)", {
149
+ parent = anchorName,
150
+ })
151
+ return nil
105
152
  end
153
+ self.debug:logTag("rules", name, "Anchoring to parent", {
154
+ parent = anchorName,
155
+ })
106
156
  local anchor = anchorApp.constructor.new(props, anchorName):render()
107
- for _, children in anchor:GetDescendants() do
108
- children:Destroy()
157
+ for _, child in anchor:GetDescendants() do
158
+ child:Destroy()
109
159
  end
110
160
  apply(anchor)({
111
161
  Name = "Anchor",
@@ -116,7 +166,7 @@ do
116
166
  local _name_1 = name
117
167
  local prev = _loaded_1[_name_1]
118
168
  if not prev then
119
- error(`Failed to retreive prev loaded data for {name}`)
169
+ error(`AppForge invariant broken: missing loaded app for {name}`, 2)
120
170
  end
121
171
  apply(prev.container)({
122
172
  [0] = anchor,
@@ -133,22 +183,19 @@ do
133
183
  local _name = name
134
184
  local loaded = _loaded[_name]
135
185
  if not loaded then
136
- error(`Failed to retreive loaded data for app: {name}`)
186
+ self.logger:log("WARN", "ZIndex skipped (app not loaded)", {
187
+ name = name,
188
+ index = index,
189
+ })
190
+ return nil
137
191
  end
192
+ self.debug:logTag("rules", name, "Applying ZIndex", {
193
+ index = index,
194
+ })
138
195
  apply(loaded.container)({
139
196
  ZIndex = index,
140
197
  })
141
198
  end
142
- function AppForge:getSource(name)
143
- local _sources = self.sources
144
- local _name = name
145
- if not (_sources[_name] ~= nil) then
146
- self:createSource(name)
147
- end
148
- local _sources_1 = self.sources
149
- local _name_1 = name
150
- return _sources_1[_name_1]
151
- end
152
199
  function AppForge:set(name, value, rules)
153
200
  if rules == nil then
154
201
  rules = true
@@ -162,10 +209,21 @@ do
162
209
  local _name_1 = name
163
210
  src = _sources_1[_name_1]
164
211
  end
165
- if src() == value then
212
+ if not src then
213
+ self.logger:log("ERROR", "Failed to set visibility (missing source)", {
214
+ name = name,
215
+ })
216
+ return nil
217
+ end
218
+ local prev = src()
219
+ if prev == value then
166
220
  return nil
167
221
  end
168
222
  src(value)
223
+ self.debug:logTag("state", name, "Visibility changed", {
224
+ from = prev,
225
+ to = value,
226
+ })
169
227
  if rules then
170
228
  self:checkRules(name)
171
229
  end
@@ -189,6 +247,7 @@ do
189
247
  self:set(name, not self:getSource(name)(), rules)
190
248
  end
191
249
  function AppForge:story(props)
250
+ self.debug:logTag("lifecycle", "story", "Creating story mount")
192
251
  local Container = create("Frame")({
193
252
  Name = "Story Container",
194
253
  BackgroundTransparency = 1,
@@ -202,6 +261,7 @@ do
202
261
  return Container
203
262
  end
204
263
  function AppForge:mount(callback, props, target)
264
+ self.debug:logTag("lifecycle", "mount", "Mounting AppForge")
205
265
  local Container = callback()
206
266
  self.innerMount = mount(function()
207
267
  apply(Container)({
@@ -212,6 +272,7 @@ do
212
272
  return self.innerMount
213
273
  end
214
274
  function AppForge:unMount()
275
+ self.debug:logTag("lifecycle", "unmount", "Unmounting AppForge")
215
276
  local _result = self.innerMount
216
277
  if _result ~= nil then
217
278
  _result()
@@ -26,7 +26,7 @@ declare namespace Types {
26
26
 
27
27
  type Class = AppProps & {
28
28
  forge: AppForge;
29
- px: typeof import("@rbxts/loners-pretty-vide-utils").px;
29
+ px: typeof import("./hooks/usePx").px;
30
30
  };
31
31
  }
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbxts/app-forge",
3
- "version": "0.7.1",
3
+ "version": "0.7.2-alpha.2",
4
4
  "description": "An App Manager for Vide",
5
5
  "main": "out/init.lua",
6
6
  "types": "out/index.d.ts",
@@ -38,11 +38,17 @@
38
38
  "@rbxts/set-timeout": "^1.1.2"
39
39
  },
40
40
  "peerDependencies": {
41
- "@rbxts/loners-pretty-react-hooks": "^0.2.9",
42
- "@rbxts/loners-pretty-vide-utils": "^0.1.7",
43
41
  "@rbxts/react": "^17.3.7-ts.1",
44
42
  "@rbxts/vide": "^0.5.7"
45
43
  },
44
+ "peerDependenciesMeta": {
45
+ "@rbxts/react": {
46
+ "optional": true
47
+ },
48
+ "@rbxts/vide": {
49
+ "optional": true
50
+ }
51
+ },
46
52
  "devDependencies": {
47
53
  "@biomejs/biome": "^2.3.7",
48
54
  "@rbxts/compiler-types": "3.0.0-types.0",