From bb9e5c2c031aae6934597c24806432be4e2a3826 Mon Sep 17 00:00:00 2001 From: znotfireman Date: Wed, 14 Jan 2026 12:37:05 +0700 Subject: [PATCH 1/2] feat: start monorepo --- .luaurc | 3 +- modules/api-types/arguments.luau | 26 + modules/api-types/core.luau | 122 + modules/api-types/gizmos.luau | 5 + modules/api-types/init.luau | 132 + modules/api-types/iris/events.luau | 87 + modules/api-types/iris/init.luau | 269 ++ modules/api-types/iris/types.luau | 105 + modules/api-types/iris/widgets/basic.luau | 39 + modules/api-types/iris/widgets/format.luau | 17 + modules/api-types/iris/widgets/image.luau | 34 + modules/api-types/iris/widgets/menu.luau | 24 + modules/api-types/iris/widgets/tab.luau | 23 + modules/api-types/iris/widgets/text.luau | 23 + modules/api-types/iris/widgets/tree.luau | 19 + modules/api-types/iris/widgets/window.luau | 33 + modules/api-types/log.luau | 16 + modules/api-types/schemas.luau | 23 + modules/api-types/utils.luau | 47 + modules/api-types/vendor/trove.luau | 116 + modules/api/init.luau | 42 + modules/plugin/.luaurc | 5 + modules/std/api.luau | 0 modules/std/core/assertCommandRuntime.luau | 16 + modules/std/functions/identity.luau | 5 + modules/std/functions/noop.luau | 4 + modules/std/history/recordChangeHistory.luau | 59 + modules/std/init.luau | 15 + modules/std/sorting/compareByTitle.luau | 7 + modules/std/vendor/.luaurc | 6 + modules/std/vendor/fzy.luau | 274 ++ modules/std/vendor/repr.luau | 339 +++ modules/std/vendor/typeforge.luau | 2587 ++++++++++++++++++ selene-std.yaml | 2 + selene.toml | 1 + src/utils/ids.luau | 1 + 36 files changed, 4525 insertions(+), 1 deletion(-) create mode 100644 modules/api-types/arguments.luau create mode 100644 modules/api-types/core.luau create mode 100644 modules/api-types/gizmos.luau create mode 100644 modules/api-types/init.luau create mode 100644 modules/api-types/iris/events.luau create mode 100644 modules/api-types/iris/init.luau create mode 100644 modules/api-types/iris/types.luau create mode 100644 modules/api-types/iris/widgets/basic.luau create mode 100644 modules/api-types/iris/widgets/format.luau create mode 100644 modules/api-types/iris/widgets/image.luau create mode 100644 modules/api-types/iris/widgets/menu.luau create mode 100644 modules/api-types/iris/widgets/tab.luau create mode 100644 modules/api-types/iris/widgets/text.luau create mode 100644 modules/api-types/iris/widgets/tree.luau create mode 100644 modules/api-types/iris/widgets/window.luau create mode 100644 modules/api-types/log.luau create mode 100644 modules/api-types/schemas.luau create mode 100644 modules/api-types/utils.luau create mode 100644 modules/api-types/vendor/trove.luau create mode 100644 modules/api/init.luau create mode 100644 modules/plugin/.luaurc create mode 100644 modules/std/api.luau create mode 100644 modules/std/core/assertCommandRuntime.luau create mode 100644 modules/std/functions/identity.luau create mode 100644 modules/std/functions/noop.luau create mode 100644 modules/std/history/recordChangeHistory.luau create mode 100644 modules/std/init.luau create mode 100644 modules/std/sorting/compareByTitle.luau create mode 100644 modules/std/vendor/.luaurc create mode 100644 modules/std/vendor/fzy.luau create mode 100644 modules/std/vendor/repr.luau create mode 100644 modules/std/vendor/typeforge.luau diff --git a/.luaurc b/.luaurc index a9e345b..416a461 100644 --- a/.luaurc +++ b/.luaurc @@ -3,7 +3,8 @@ "aliases": { "src": "src", "include": "include", - "pkgs": "roblox_packages" + "pkgs": "roblox_packages", + "modules": "modules" }, "lint": { "*": true diff --git a/modules/api-types/arguments.luau b/modules/api-types/arguments.luau new file mode 100644 index 0000000..506b0be --- /dev/null +++ b/modules/api-types/arguments.luau @@ -0,0 +1,26 @@ +local schemas = require("./schemas") +local utils = require("./utils") + +export type BaseArgument = { + type: Type, + + name: string, + description: schemas.Description, + placeholder: string, + required: boolean?, +} + +export type TextArgument = utils.MergeTable, { + sensitive: boolean?, +}> + +export type InstanceClassArgument = BaseArgument<"instance-class"> + +export type SelectArgumentOption = { title: string, value: string } +export type SelectArgument = utils.MergeTable, { + options: { SelectArgumentOption }, +}> + +export type Argument = TextArgument | InstanceClassArgument | SelectArgument + +return nil diff --git a/modules/api-types/core.luau b/modules/api-types/core.luau new file mode 100644 index 0000000..689dd8c --- /dev/null +++ b/modules/api-types/core.luau @@ -0,0 +1,122 @@ +local arguments = require("./arguments") +local iris = require("./iris") +local log = require("./log") +local schemas = require("./schemas") +local trove = require("./vendor/trove") + +export type RecordChangeHistoryOptions = { + name: string?, + displayName: string?, + + ignoreRobloxFailure: boolean?, + hideRobloxFailure: boolean?, + isCancellable: boolean?, + cancelsOthers: boolean?, +} + +export type CommandRuntimeSelf = { + trove: trove.Trove, + command: Command, + log: log.Log, +} + +export type CommandRuntimeMetatable = { + __index: CommandRuntimeMetatable, + __tostring: (self: CommandRuntime) -> string, + + Iris: iris.Iris, + + getTextArgument: (self: CommandRuntime, text: string) -> (), + + cleanup: (self: CommandRuntime) -> (), + getSelection: (self: CommandRuntime) -> { Instance }, + setSelection: (self: CommandRuntime, instances: { Instance }) -> (), + recordChangeHistory: (self: CommandRuntime, maybeOptions: RecordChangeHistoryOptions?) -> (), +} + +export type CommandRuntime = setmetatable + +--- + +export type CommandOutput = { + renderViewport: () -> (), + + windowArgs: {}, + windowConfig: {}, + renderWindow: (window: any) -> (), +} + +--- + +export type CommandSchema = { + id: string, + title: string, + description: schemas.Description, + icon: string?, + contributors: { schemas.Contributor }?, + arguments: { arguments.Argument }?, +} + +export type CommandSelf = { + extension: Extension, + schema: CommandSchema, +} + +export type CommandMetatable = { + __index: CommandMetatable, + __tostring: (self: Command) -> string, + + formatFullId: (self: Command) -> string, +} + +export type Command = setmetatable + +export type CommandRunner = + | ((r: CommandRuntime) -> ()) + | ((r: CommandRuntime) -> CommandOutput) + | ((r: CommandRuntime) -> CommandOutput?) + +--- + +export type ExtensionCapabilities = { + commands: { + [string]: { reads: boolean, runs: boolean }, + }, +} + +export type ExtensionSchema = { + id: string, + title: string, + description: schemas.Description, + icon: string, + contributors: { schemas.Contributor }?, + capabilities: ExtensionCapabilities?, +} + +export type ExtensionSelf = { + trove: trove.Trove, + schema: ExtensionSchema, +} + +export type ExtensionMetatable = { + __index: ExtensionMetatable, + __tostring: (self: Extension) -> string, + + getCommands: (self: Extension) -> { Command }, + pushCommand: (self: Extension, command: Command) -> Command, + newCommand: (self: Extension, schema: CommandSchema, runner: CommandRunner) -> Command, +} + +export type Extension = setmetatable + +--- + +--[=[ + @type FromExtension Extension | Command + + Products that an Extension can produce, which is usable with the Rocket + plugin. +]=] +export type FromExtension = Extension | Command + +return nil diff --git a/modules/api-types/gizmos.luau b/modules/api-types/gizmos.luau new file mode 100644 index 0000000..b6118df --- /dev/null +++ b/modules/api-types/gizmos.luau @@ -0,0 +1,5 @@ +export type Gizmos = { + drawLine: (from: CFrame, to: CFrame) -> (), +} + +return nil diff --git a/modules/api-types/init.luau b/modules/api-types/init.luau new file mode 100644 index 0000000..1aabac9 --- /dev/null +++ b/modules/api-types/init.luau @@ -0,0 +1,132 @@ +-- NOTE: this module does not run through darklua so we can use type functions + +local arguments = require("@self/arguments") +local core = require("@self/core") +local iris = require("@self/iris") +local log = require("@self/log") +local schemas = require("@self/schemas") +local utils = require("@self/utils") + +export type FromExtension = core.FromExtension + +export type Extension = core.Extension +export type ExtensionSelf = core.ExtensionSelf +export type ExtensionMetatable = core.ExtensionMetatable +export type ExtensionSchema = core.ExtensionSchema + +export type Command = core.Command +export type CommandSelf = core.CommandSelf +export type CommandMetatable = core.CommandMetatable +export type CommandSchema = core.CommandSchema +export type CommandRunner = core.CommandRunner +export type CommandOutput = core.CommandOutput +export type CommandRuntime = core.CommandRuntime + +export type BaseArgument = arguments.BaseArgument +export type TextArgument = arguments.TextArgument +export type InstanceClassArgument = arguments.InstanceClassArgument +export type SelectArgumentOption = arguments.SelectArgumentOption +export type SelectArgument = arguments.SelectArgument +export type Argument = arguments.Argument + +export type Description = schemas.Description +export type UserContributor = schemas.UserContributor +export type CustomContributor = schemas.CustomContributor +export type Contributor = schemas.Contributor + +export type Iris = iris.Iris +export type IrisWidgets = iris.Widgets +export type IrisWidget = iris.Widget +export type IrisState = iris.State +export type IrisConfig = iris.Config +export type IrisPartialConfig = iris.PartialConfig +export type GetIrisWidget = iris.GetWidget + +export type Log = log.Log + +export type MergeTable = utils.MergeTable +export type IntersectionToTable = utils.IntersectionToTable + +--[=[ + @class Rocket + @since 0.1.0 + + The Rocket extension library, which is inserted by the Rocket plugin as + the `game.Rocket` ModuleScript. + + Below is the recommended way to require Rocket: + + ```lua + local Rocket = require(game:WaitForChild("Rocket", math.huge)) + print(Rocket.version) + ``` + + ## Purpose + + This package only provides typings for the Rocket extension library. The + static properties are inserted at runtime by the Rocket plugin. + + ```lua + -- Ignorance is strength, let's consume Rocket as a package! + local Plugin = script:FindFirstAncestorOfClass("Plugin") + local Rocket = require(Plugin.Packages.Rocket) + + -- This will error. + local ext = Rocket:newExtension(...) + ``` + + ## Usage from Package Managers + + If you would like to access Rocket with a sync-tool like Rojo, you can + either: + + 1. In your project file for your sync tool's sourcemap, such as Rojo, add + an entry for `Rocket`: + + ```jsonc + // It is imperative this only exists in your sourcemap. You may make a + // `sourcemap.project.json` file like below and generate a sourcemap + // from that. + // + // Otherwise, this will override Rocket's actual extension library at + // runtime, which would not end well. + { + "name": "My Plugin", + "tree": { + "$className": "DataModel" + "Rocket": { + "$path": "./roblox_packages/Rocket" + }, + + // Then, place your plugin somewhere else. + "ServerScriptService": { + "Plugin": { + "$path": "./src" + }, + } + } + } + ``` + + Then require Rocket like in Studio: + + ```lua + local Rocket = require(game:WaitForChild("Rocket", math.huge)) + print(Rocket.version) + ``` + + 2. Install Rocket's `api` package with your preferred package manager, + which wraps the require call above with the library's types. +]=] +export type Rocket = { + version: string, + + useIris: (self: Rocket) -> iris.Iris?, + useCurrentCommand: (self: Rocket) -> Command?, + useCurrentCommandRuntime: (self: Rocket) -> CommandRuntime?, + useCurrentExtension: (self: Rocket) -> Extension?, + + newExtension: (self: Rocket, plugin: Plugin, schema: ExtensionSchema) -> Extension, +} + +return {} :: Rocket diff --git a/modules/api-types/iris/events.luau b/modules/api-types/iris/events.luau new file mode 100644 index 0000000..c68cc94 --- /dev/null +++ b/modules/api-types/iris/events.luau @@ -0,0 +1,87 @@ +export type Hovered = { + isHoveredEvent: boolean, + hovered: () -> boolean, +} + +export type Clicked = { + lastClickedTick: number, + clicked: () -> boolean, +} + +export type RightClicked = { + lastRightClickedTick: number, + rightClicked: () -> boolean, +} + +export type DoubleClicked = { + lastClickedTime: number, + lastClickedPosition: Vector2, + lastDoubleClickedTick: number, + doubleClicked: () -> boolean, +} + +export type CtrlClicked = { + lastCtrlClickedTick: number, + ctrlClicked: () -> boolean, +} + +export type Active = { + active: () -> boolean, +} + +export type Checked = { + lastCheckedTick: number, + checked: () -> boolean, +} + +export type Unchecked = { + lastUncheckedTick: number, + unchecked: () -> boolean, +} + +export type Opened = { + lastOpenedTick: number, + opened: () -> boolean, +} + +export type Closed = { + lastClosedTick: number, + closed: () -> boolean, +} + +export type Collapsed = { + lastCollapsedTick: number, + collapsed: () -> boolean, +} + +export type Uncollapsed = { + lastUncollapsedTick: number, + uncollapsed: () -> boolean, +} + +export type Selected = { + lastSelectedTick: number, + selected: () -> boolean, +} + +export type Unselected = { + lastUnselectedTick: number, + unselected: () -> boolean, +} + +export type Changed = { + lastChangedTick: number, + changed: () -> boolean, +} + +export type NumberChanged = { + lastNumberChangedTick: number, + numberChanged: () -> boolean, +} + +export type TextChanged = { + lastTextChangedTick: number, + textChanged: () -> boolean, +} + +return nil diff --git a/modules/api-types/iris/init.luau b/modules/api-types/iris/init.luau new file mode 100644 index 0000000..863edf6 --- /dev/null +++ b/modules/api-types/iris/init.luau @@ -0,0 +1,269 @@ +-- Iris new solver typings for Rocket based from upstream: +-- https://github.com/SirMallard/Iris/ + +local base = require("@self/types") +local basic = require("@self/widgets/basic") +local format = require("@self/widgets/format") +local image = require("@self/widgets/image") +local menu = require("@self/widgets/menu") +local tab = require("@self/widgets/tab") +local text = require("@self/widgets/text") +local tree = require("@self/widgets/tree") +local utils = require("./utils") +local window = require("@self/widgets/window") + +-- TODO: Input, Drag, Slider, Plot, Combo, and Table widgets +export type Widgets = { + Root: window.Root, + Window: window.Window, + Tooltip: window.Tooltip, + + Menu: menu.Menu, + MenuBar: menu.MenuBar, + MenuItem: menu.MenuItem, + MenuToggle: menu.MenuToggle, + + Separator: format.Separator, + Indent: format.Indent, + SameLine: format.SameLine, + Group: format.Group, + + Text: text.Text, + SeparatorText: text.SeparatorText, + InputText: text.InputText, + + Button: basic.Button, + Checkbox: basic.Checkbox, + RadioButton: basic.RadioButton, + SmallButton: basic.SmallButton, + + CollapsingHeader: tree.CollapsingHeader, + Tree: tree.Tree, + + TabBar: tab.TabBar, + Tab: tab.Tab, + + Image: image.Image, + ImageButton: image.ImageButton, +} + +type IntersectionToTable = utils.IntersectionToTable + +type function IntoArguments(widgets: type) + assert(widgets.tag == "table", "expected table") + local arguments = types.singleton("arguments") + local result = types.newtable() + + for name, values in widgets:properties() do + assert(name.tag == "singleton", `invalid key of tag {name.tag}`) + + local maybeWidget = values.read + assert(maybeWidget, `widget "{name}" cannot be read-only`) + local widgetTag = maybeWidget.tag + local widget = if widgetTag == "table" + then maybeWidget + elseif widgetTag == "intersection" then IntersectionToTable(maybeWidget) + else error(`unsupported widget of tag {widgetTag}`) + + local widgetKeys = types.newtable() + + local widgetArguments = widget:readproperty(arguments) + if widgetArguments then + assert(widgetArguments.tag == "table", `invalid widget arguments for "{widget}"`) + + for argument in widgetArguments:properties() do + widgetKeys:setreadproperty(argument, types.number) + end + end + + result:setreadproperty(name, widgetKeys) + end + + return result +end + +type function IntoConstructors(widgets: type) + assert(widgets.tag == "table", "expected table") + + local KEY_ARGUMENTS = types.singleton("arguments") + local KEY_STATES = types.singleton("state") + local KEY_VALUE = types.singleton("value") + + local TYPE_NIL = types.singleton(nil) + local TYPE_WIDGET_ARGUMENTS = types.newtable( + {}, + { index = types.number, readresult = types.any, writeresult = types.any } + ) + + local result = types.newtable() + + for name, values in widgets:properties() do + assert(name.tag == "singleton", `invalid key of tag {name.tag}`) + + local maybeWidget = values.read + assert(maybeWidget, `widget "{name:value()}" cannot be read-only`) + + local widgetTag = maybeWidget.tag + local widget = if widgetTag == "table" + then maybeWidget + elseif widgetTag == "intersection" then IntersectionToTable(maybeWidget) + else error(`unsupported widget "{name:value()}" of tag {widgetTag}`) + + local parameters = {} + + local widgetArguments = widget:readproperty(KEY_ARGUMENTS) + parameters[1] = if widgetArguments and next(widgetArguments:properties()) + then TYPE_WIDGET_ARGUMENTS + else TYPE_NIL + + local widgetStates = widget:readproperty(KEY_STATES) + local widgetStatesProperties = ( + widgetStates and widgetStates:properties() + ) :: { [type]: { read: type?, write: type? } } + + if widgetStates and next(widgetStatesProperties) then + for stateName, stateValues in widgetStatesProperties do + local state = stateValues.read + assert(state, "state cannot be write-only") + assert(state.tag == "table", `invalid state "{stateName:value()}" in widget "{name:value()}"`) + + local innerState = state:readproperty(KEY_VALUE) + assert(innerState, "state missing value property") + + widgetStates:setproperty(stateName, types.unionof(innerState, state, TYPE_NIL)) + end + + parameters[2] = types.optional(widgetStates) + else + parameters[2] = TYPE_NIL + end + + local realParameterLen = 0 + for _, parameter in parameters do + if parameter.tag == "nil" or (parameter.tag == "singleton" and parameter:value() == nil) then + break + end + + realParameterLen += 1 + end + + local realParameters = table.move(parameters, 1, realParameterLen, 1, {}) + + result:setreadproperty(name, types.newfunction({ head = realParameters }, { head = { widget } })) + end + + return result +end + +export type ID = base.ID +export type State = base.State +export type Widget = base.Widget +export type ParentWidget = base.ParentWidget + +export type WidgetArguments = IntoArguments +export type WidgetConstructors = IntoConstructors +export type GetWidget = index + +export type Config = {} +export type PartialConfig = {} + +export type Internal = {} + +--[=[ + @class Iris + @since 0.1.0 + @tag vendor:https://sirmallard.github.io/Iris/ + + Iris is the immediate-mode GUI library used in Rocket. For more information, + see Iris' documentation and API reference. + + Note that Rocket derives it's own typings for Iris that are usable with + Luau's new solver. This enables stronger type-safety: + + ```luau + -- With the original Iris package, `Iris.Args` would not autocomplete, and + -- the states argument will throw a type error not knowing that non-state + -- values can be used. + Iris.Window( + { "Hello Rocket", [Iris.Args.Window.NoMove] = true }, + { position = UserInputService:GetMouseLocation() } + ) + ``` + + Types from the Iris package will not align with types from the Rocket + library. In particular, Rocket does not export individual widget types, and + Rocket prefixes Iris types with "Iris". + + Types for widgets must be accessed by indexing the Widgets type: + + ```luau + type Window = index + + -- alternatively + type Window = Rocket.GetIrisWidget<"Window"> + ``` + + More convenient widget types may be considered in the future. + + @example Using Iris in a command + + ```lua + ext:newCommand({ + id = "show-demo-window", + title = "Show Demo Window", + description = "Shows Iris' demo window." + }, function(r: Rocket.CommandRuntime) + local Iris = r.Iris + local output = r:newCommandOutput() + + function output.renderViewport() + if Iris.Button({ "Stop Demo Window" }).clicked() then + r:cleanup() + return + end + + Iris.ShowDemoWindow() + end + + return output + end) + ``` +]=] +export type Iris = WidgetConstructors & { + Internal: Internal, + + Args: WidgetArguments, + Disabled: boolean, + TemplateConfig: { + colorDark: PartialConfig, + colorLight: PartialConfig, + sizeDefault: PartialConfig, + sizeClear: PartialConfig, + utilityDefault: PartialConfig, + }, + + Connect: (self: Iris, callback: () -> ()) -> () -> (), + Append: (userInstance: Instance) -> (), + ComputedState: (state: State, onChangeCallback: (stateValue: T) -> U) -> State, + End: () -> (), + ForceRefresh: () -> (), + Init: ( + parentInstance: Instance?, + eventConnection: RBXScriptSignal | () -> () | false | nil, + allowMultipleInits: boolean? + ) -> (), + PopConfig: () -> (), + PopID: () -> (), + PushConfig: (deltaStyles: PartialConfig) -> (), + PushId: (id: ID) -> (), + SetNextWidgetID: (id: ID) -> (), + ShowDemoWindow: () -> (), + Shutdown: () -> (), + State: ((initialValue: T) -> State) & (() -> State), + TableState: (tbl: { [K]: V }, key: K, callback: ((newValue: V) -> false?)?) -> State, + UpdateGlobalConfig: (deltaStyle: PartialConfig) -> (), + VariableState: (variable: T, callback: (T) -> ()) -> State, + WeakState: (initialValue: T) -> State, +} + +return nil diff --git a/modules/api-types/iris/types.luau b/modules/api-types/iris/types.luau new file mode 100644 index 0000000..de4c170 --- /dev/null +++ b/modules/api-types/iris/types.luau @@ -0,0 +1,105 @@ +--#selene: allow(undefined_variable) + +export type ID = string + +export type State = { + ID: ID, + value: T, + lastChangeTick: number, + ConnectedWidgets: { [ID]: Widget }, + ConnectedFunctions: { (newValue: T) -> () }, + + get: (self: State) -> T, + set: (self: State, newValue: T, force: true?) -> (), + onChange: (self: State, onChangeCallback: (newValue: T) -> ()) -> () -> (), + changed: (self: State) -> boolean, +} + +export type function Statetify(states: type) + assert(states.tag == "table", "expected table") + + for key, values in states:properties() do + -- ur joking + -- states:setproperty(key, State(values.read)) + + local valueType = values.read + assert(valueType, `value for state "{key}" cannot be write-only`) + + local connectedFunction = types.newfunction({ head = { valueType } }) + + local state = types.newtable { + [types.singleton("ID")] = types.string, + [types.singleton("value")] = valueType, + [types.singleton("lastChangeTick")] = types.number, + [types.singleton("ConnectedWidgets")] = types.newtable({}, { + index = types.number, + + -- TODO: how do we get the Widget type? + -- ...I'm not going to build it by hand bro + readresult = types.any, + writeresult = types.any, + }), + [types.singleton("ConnectedFunctions")] = types.newtable({}, { + index = types.string, + readresult = connectedFunction, + writeresult = connectedFunction, + }), + } + + state:setproperty( + types.singleton("get"), + types.newfunction({ + head = { state }, + }, { head = { valueType } }) + ) + + state:setproperty( + types.singleton("set"), + types.newfunction({ + head = { state, valueType, types.optional(types.singleton(true)) }, + }) + ) + + state:setproperty( + types.singleton("onChange"), + types.newfunction({ + head = { state, types.newfunction({ head = { valueType } }) }, + }, { head = { types.newfunction() } }) + ) + + state:setproperty( + types.singleton("changed"), + types.newfunction({ + head = { state }, + }, { head = { types.boolean } }) + ) + + states:setproperty(key, state) + end + + return states +end + +export type Widget = { + ID: ID, + type: string, + lastCycleTick: number, + trackedEvents: {}, + parentWidget: ParentWidget, + + arguments: Arguments, + providedArguments: {}, + + state: States, + + Instance: GuiObject, + ZIndex: number, +} + +export type ParentWidget = Widget & { + ChildContainer: GuiObject, + ZOffset: number, + ZUpdate: boolean, +} + +return nil diff --git a/modules/api-types/iris/widgets/basic.luau b/modules/api-types/iris/widgets/basic.luau new file mode 100644 index 0000000..dedcdf1 --- /dev/null +++ b/modules/api-types/iris/widgets/basic.luau @@ -0,0 +1,39 @@ +local events = require("../events") +local types = require("../types") + +export type Button = + types.Widget<{ Text: string, Size: UDim2? }> + & events.Clicked + & events.RightClicked + & events.DoubleClicked + & events.CtrlClicked + & events.Hovered + +export type Checkbox = + types.Widget<{ Text: string }, { isChecked: types.State }> + & events.Checked + & events.Unchecked + & events.Hovered + +export type RadioButton = + types.Widget<{ + Text: string, + Index: any, + }, { index: types.State }> + & events.Selected + & events.Unselected + & events.Active + & events.Hovered + +export type SmallButton = + types.Widget<{ + Text: string, + Size: UDim2?, + }> + & events.Clicked + & events.RightClicked + & events.DoubleClicked + & events.CtrlClicked + & events.Hovered + +return nil diff --git a/modules/api-types/iris/widgets/format.luau b/modules/api-types/iris/widgets/format.luau new file mode 100644 index 0000000..5bbdabe --- /dev/null +++ b/modules/api-types/iris/widgets/format.luau @@ -0,0 +1,17 @@ +local types = require("../types") + +export type Separator = types.Widget + +export type Indent = types.ParentWidget<{ + Width: number?, +}> + +export type SameLine = types.ParentWidget<{ + Width: number?, + VerticalAlignment: Enum.VerticalAlignment?, + HorizontalAlignment: Enum.HorizontalAlignment?, +}> + +export type Group = types.ParentWidget + +return nil diff --git a/modules/api-types/iris/widgets/image.luau b/modules/api-types/iris/widgets/image.luau new file mode 100644 index 0000000..a53157a --- /dev/null +++ b/modules/api-types/iris/widgets/image.luau @@ -0,0 +1,34 @@ +local events = require("../events") +local types = require("../types") + +export type Image = + types.Widget<{ + Image: string, + Size: UDim2, + Rect: Rect?, + ScaleType: Enum.ScaleType?, + ResampleMode: Enum.ResamplerMode?, + TileSize: UDim2?, + SliceCenter: Rect?, + SliceScale: number?, + }> + & events.Hovered + +export type ImageButton = + types.Widget<{ + Image: string, + Size: UDim2, + Rect: Rect?, + ScaleType: Enum.ScaleType?, + ResampleMode: Enum.ResamplerMode?, + TileSize: UDim2?, + SliceCenter: Rect?, + SliceScale: number?, + }> + & events.Clicked + & events.RightClicked + & events.DoubleClicked + & events.CtrlClicked + & events.Hovered + +return nil diff --git a/modules/api-types/iris/widgets/menu.luau b/modules/api-types/iris/widgets/menu.luau new file mode 100644 index 0000000..0abd505 --- /dev/null +++ b/modules/api-types/iris/widgets/menu.luau @@ -0,0 +1,24 @@ +local events = require("../events") +local types = require("../types") + +export type MenuBar = types.ParentWidget + +export type Menu = types.Widget<{ Text: string? }, { isOpened: types.State }> + +export type MenuItem = types.Widget<{ + Text: string, + KeyCode: Enum.KeyCode?, + ModifierKey: Enum.ModifierKey?, +}> + +export type MenuToggle = + types.Widget<{ + Text: string, + KeyCode: Enum.KeyCode?, + ModifierKey: Enum.ModifierKey?, + }, { isChecked: types.State }> + & events.Checked + & events.Unchecked + & events.Hovered + +return nil diff --git a/modules/api-types/iris/widgets/tab.luau b/modules/api-types/iris/widgets/tab.luau new file mode 100644 index 0000000..5341531 --- /dev/null +++ b/modules/api-types/iris/widgets/tab.luau @@ -0,0 +1,23 @@ +local events = require("../events") +local types = require("../types") + +export type TabBar = types.ParentWidget<{}, { + index: types.State, +}> + +export type Tab = + types.Widget<{ + Text: string, + Hideable: boolean?, + }, { + isOpened: types.State, + }> + & events.Clicked + & events.Hovered + & events.Selected + & events.Unselected + & events.Active + & events.Opened + & events.Closed + +return nil diff --git a/modules/api-types/iris/widgets/text.luau b/modules/api-types/iris/widgets/text.luau new file mode 100644 index 0000000..59e7bb5 --- /dev/null +++ b/modules/api-types/iris/widgets/text.luau @@ -0,0 +1,23 @@ +local events = require("../events") +local types = require("../types") + +export type Text = + types.Widget<{ + Text: string?, + Wrapped: boolean?, + Color: Color3?, + RichText: boolean?, + }> + & events.Hovered + +export type SeparatorText = types.Widget<{ Text: string? }> & events.Hovered + +export type InputText = + types.Widget< + { Text: string?, TextHint: string?, ReadOnly: string?, MultiLine: string? }, + { text: types.State } + > + & events.Hovered + & events.TextChanged + +return nil diff --git a/modules/api-types/iris/widgets/tree.luau b/modules/api-types/iris/widgets/tree.luau new file mode 100644 index 0000000..befbb3c --- /dev/null +++ b/modules/api-types/iris/widgets/tree.luau @@ -0,0 +1,19 @@ +local events = require("../events") +local types = require("../types") + +export type CollapsingHeader = + types.Widget<{ Text: string, DefaultOpen: boolean? }, { isUncollapsed: types.State }> + & events.Collapsed + & events.Uncollapsed + & events.Hovered + +export type Tree = + types.Widget< + { Text: string, SpanAvailWidth: boolean?, NoIndent: boolean?, DefaultOpen: boolean? }, + { isUncollapsed: types.State } + > + & events.Collapsed + & events.Uncollapsed + & events.Hovered + +return nil diff --git a/modules/api-types/iris/widgets/window.luau b/modules/api-types/iris/widgets/window.luau new file mode 100644 index 0000000..a4d8145 --- /dev/null +++ b/modules/api-types/iris/widgets/window.luau @@ -0,0 +1,33 @@ +local events = require("../events") +local types = require("../types") + +export type Root = types.ParentWidget + +export type Window = + types.Widget<{ + Title: string?, + NoTitleBar: boolean?, + NoBackground: boolean?, + NoCollapse: boolean?, + NoClose: boolean?, + NoMove: boolean?, + NoScrollbar: boolean?, + NoResize: boolean?, + NoNav: boolean?, + NoMenu: boolean?, + }, { + size: types.State, + position: types.State, + isUncollapsed: types.State, + isOpened: types.State, + scrollDistance: types.State, + }> + & events.Opened + & events.Closed + & events.Collapsed + & events.Uncollapsed + & events.Hovered + +export type Tooltip = types.Widget<{ Text: string }> + +return nil diff --git a/modules/api-types/log.luau b/modules/api-types/log.luau new file mode 100644 index 0000000..65b4304 --- /dev/null +++ b/modules/api-types/log.luau @@ -0,0 +1,16 @@ +export type LogSelf = {} + +export type LogMetatable = { + __index: LogMetatable, + __tostring: LogMetatable, + + trace: (self: Log, ...unknown) -> (), + debug: (self: Log, ...unknown) -> (), + info: (self: Log, ...unknown) -> (), + warn: (self: Log, ...unknown) -> (), + error: (self: Log, ...unknown) -> (), +} + +export type Log = setmetatable + +return nil diff --git a/modules/api-types/schemas.luau b/modules/api-types/schemas.luau new file mode 100644 index 0000000..3bbcdca --- /dev/null +++ b/modules/api-types/schemas.luau @@ -0,0 +1,23 @@ +local utils = require("./utils") + +export type Description = nil | string | { + brief: string?, + long: string?, +} + +export type BaseContributor = { + type: Type, +} + +export type UserContributor = utils.MergeTable, { + id: number, +}> + +export type CustomContributor = utils.MergeTable, { + image: number, + title: number, +}> + +export type Contributor = UserContributor | CustomContributor + +return nil diff --git a/modules/api-types/utils.luau b/modules/api-types/utils.luau new file mode 100644 index 0000000..a2302b2 --- /dev/null +++ b/modules/api-types/utils.luau @@ -0,0 +1,47 @@ +--#selene: allow(undefined_variable) + +export type function MergeTable(base: type, override: type) + assert(base.tag == "table", "expected base to be a table") + assert(override.tag == "table", "expected override to be a table") + + local result = types.newtable() + for _, merge in { base, override } do + for key, values in merge:properties() do + if values.read and values.write then + result:setproperty(key, values.read) + elseif values.read then + result:setreadproperty(key, values.read) + elseif values.write then + result:setreadproperty(key, values.write) + end + end + end + + return result +end + +export type function IntersectionToTable(intersection: type): type + assert(intersection.tag == "intersection", "expected an intersection") + + local result = types.newtable() + for index, component in intersection:components() do + local merge = if component.tag == "intersection" + then (IntersectionToTable(component) :: type):properties() + elseif component.tag == "table" then component:properties() + else error(`unsupported type "{component.tag}" in component #{index}`) + + for key, values in merge do + if values.read and values.write then + result:setproperty(key, values.read) + elseif values.read then + result:setreadproperty(key, values.read) + elseif values.write then + result:setreadproperty(key, values.write) + end + end + end + + return result +end + +return nil diff --git a/modules/api-types/vendor/trove.luau b/modules/api-types/vendor/trove.luau new file mode 100644 index 0000000..3b85510 --- /dev/null +++ b/modules/api-types/vendor/trove.luau @@ -0,0 +1,116 @@ +export type Trove = { + Extend: (self: Trove) -> Trove, + Clone: (self: Trove, instance: T & Instance) -> T, + Construct: (self: Trove, class: Constructable, A...) -> T, + Connect: ( + self: Trove, + signal: SignalLike | SignalLikeMetatable | RBXScriptSignal, + fn: (...any) -> ...any + ) -> ConnectionLike | ConnectionLikeMetatable, + BindToRenderStep: (self: Trove, name: string, priority: number, fn: (dt: number) -> ()) -> (), + AddPromise: (self: Trove, promise: (T & PromiseLike) | (T & PromiseLikeMetatable)) -> T, + Add: (self: Trove, object: T & Trackable, cleanupMethod: string?) -> T, + Remove: (self: Trove, object: T & Trackable) -> boolean, + Pop: (self: Trove, object: T & Trackable) -> boolean, + Clean: (self: Trove) -> (), + WrapClean: (self: Trove) -> () -> (), + AttachToInstance: (self: Trove, instance: Instance) -> RBXScriptConnection, + Destroy: (self: Trove) -> (), +} + +export type TroveInternal = Trove & { + _objects: { any }, + _cleaning: boolean, + _findAndRemoveFromObjects: (self: TroveInternal, object: any, cleanup: boolean) -> boolean, + _cleanupObject: (self: TroveInternal, object: any, cleanupMethod: string?) -> (), +} + +export type Trackable = + | Instance + | RBXScriptConnection + | ConnectionLike + | ConnectionLikeMetatable + | PromiseLike + | PromiseLikeMetatable + | thread + | ((...unknown) -> ...unknown) + | (() -> ()) + | Destroyable + | DestroyableMetatable + | DestroyableLowercase + | DestroyableLowercaseMetatable + | Disconnectable + | DisconectableMetatable + | DisconnectableLowercase + | DisconnectableLowercaseMetatable + | SignalLike + | SignalLikeMetatable + +export type ConnectionLike = { + Connected: boolean, + Disconnect: (self: ConnectionLike) -> (), +} + +export type ConnectionLikeMetatable = typeof(setmetatable( + {}, + {} :: { Connected: boolean, Disconnect: (self: ConnectionLikeMetatable) -> () } +)) + +export type SignalLike = { + Connect: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike | ConnectionLikeMetatable, + Once: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike | ConnectionLikeMetatable, +} + +export type SignalLikeMetatable = typeof(setmetatable( + {}, + {} :: { + Connect: (self: SignalLikeMetatable, callback: (...any) -> ...any) -> ConnectionLike | ConnectionLikeMetatable, + Once: (self: SignalLikeMetatable, callback: (...any) -> ...any) -> ConnectionLike | ConnectionLikeMetatable, + } +)) + +export type PromiseLike = { + getStatus: (self: PromiseLike) -> string, + finally: (self: PromiseLike, callback: (...any) -> ...any) -> PromiseLike | PromiseLikeMetatable, + cancel: (self: PromiseLike) -> (), +} + +export type PromiseLikeMetatable = typeof(setmetatable( + {}, + {} :: { + getStatus: (self: any) -> string, + finally: (self: PromiseLikeMetatable, callback: (...any) -> ...any) -> PromiseLike | PromiseLikeMetatable, + cancel: (self: PromiseLikeMetatable) -> (), + } +)) + +export type Constructable = { new: (A...) -> T } | (A...) -> T + +export type Destroyable = { + Destroy: (self: Destroyable) -> (), +} + +export type DestroyableMetatable = setmetatable<{}, { destroy: (self: Destroyable) -> () }> + +export type DestroyableLowercase = { + destroy: (self: DestroyableLowercase) -> (), +} + +export type DestroyableLowercaseMetatable = setmetatable<{}, { destroy: (self: DestroyableLowercaseMetatable) -> () }> + +export type Disconnectable = { + Disconnect: (self: Disconnectable) -> (), +} + +export type DisconectableMetatable = setmetatable<{}, { Disconnect: (self: DisconectableMetatable) -> () }> + +export type DisconnectableLowercase = { + disconnect: (self: DisconnectableLowercase) -> (), +} + +export type DisconnectableLowercaseMetatable = setmetatable< + {}, + { disconnect: (self: DisconnectableLowercaseMetatable) -> () } +> + +return nil diff --git a/modules/api/init.luau b/modules/api/init.luau new file mode 100644 index 0000000..6b3acf3 --- /dev/null +++ b/modules/api/init.luau @@ -0,0 +1,42 @@ +local types = require("./api-types") + +export type FromExtension = types.FromExtension + +export type Extension = types.Extension +export type ExtensionSelf = types.ExtensionSelf +export type ExtensionMetatable = types.ExtensionMetatable +export type ExtensionSchema = types.ExtensionSchema + +export type Command = types.Command +export type CommandSelf = types.CommandSelf +export type CommandMetatable = types.CommandMetatable +export type CommandSchema = types.CommandSchema +export type CommandRunner = types.CommandRunner +export type CommandOutput = types.CommandOutput +export type CommandRuntime = types.CommandRuntime + +export type BaseArgument = types.BaseArgument +export type TextArgument = types.TextArgument +export type InstanceClassArgument = types.InstanceClassArgument +export type SelectArgumentOption = types.SelectArgumentOption +export type SelectArgument = types.SelectArgument +export type Argument = types.Argument + +export type Description = types.Description +export type UserContributor = types.UserContributor +export type CustomContributor = types.CustomContributor +export type Contributor = types.Contributor + +export type Iris = types.Iris +export type IrisWidgets = types.IrisWidgets +export type IrisWidget = types.IrisWidget +export type IrisState = types.IrisState +export type IrisConfig = types.IrisConfig +export type IrisPartialConfig = types.IrisPartialConfig +export type GetIrisWidget = types.GetIrisWidget + +export type MergeTable = types.MergeTable +export type IntersectionToTable = types.IntersectionToTable + +local Rocket: types.Rocket = (require)(game:WaitForChild("Rocket")) +return Rocket diff --git a/modules/plugin/.luaurc b/modules/plugin/.luaurc new file mode 100644 index 0000000..e4674f1 --- /dev/null +++ b/modules/plugin/.luaurc @@ -0,0 +1,5 @@ +{ + "aliases": { + "root": "." + } +} diff --git a/modules/std/api.luau b/modules/std/api.luau new file mode 100644 index 0000000..e69de29 diff --git a/modules/std/core/assertCommandRuntime.luau b/modules/std/core/assertCommandRuntime.luau new file mode 100644 index 0000000..1fcfc6e --- /dev/null +++ b/modules/std/core/assertCommandRuntime.luau @@ -0,0 +1,16 @@ +local Rocket = require("@modules/api") + +local function assertCommandRuntime(api: string?) + local r = Rocket:useCurrentCommandRuntime() + if r then + return r + end + + local errorMessage = if api + then `{api} can only be used inside a command.` + else "This can only be called inside a command." + + error(errorMessage, 2) +end + +return assertCommandRuntime diff --git a/modules/std/functions/identity.luau b/modules/std/functions/identity.luau new file mode 100644 index 0000000..d2a5693 --- /dev/null +++ b/modules/std/functions/identity.luau @@ -0,0 +1,5 @@ +local function identity(...: T...) + return ... +end + +return identity diff --git a/modules/std/functions/noop.luau b/modules/std/functions/noop.luau new file mode 100644 index 0000000..c5db247 --- /dev/null +++ b/modules/std/functions/noop.luau @@ -0,0 +1,4 @@ +-- selene: allow(unused_variable) +local function noop(...: T...) end + +return noop diff --git a/modules/std/history/recordChangeHistory.luau b/modules/std/history/recordChangeHistory.luau new file mode 100644 index 0000000..f3f5dfe --- /dev/null +++ b/modules/std/history/recordChangeHistory.luau @@ -0,0 +1,59 @@ +local ChangeHistoryService = game:GetService("ChangeHistoryService") +local RunService = game:GetService("RunService") + +local assertCommandRuntime = require("@modules/std/core/assertCommandRuntime") + +export type RecordChangeHistoryOptions = { + name: string?, + displayName: string?, + + ignoreRobloxFailure: boolean?, + hideRobloxFailure: boolean?, + isCancellable: boolean?, + cancelsOthers: boolean?, + + hideToast: boolean?, +} + +local function recordChangeHistory( + recordingOptions: RecordChangeHistoryOptions? +): (operation: Enum.FinishRecordingOperation?) -> () + local r = assertCommandRuntime("recordChangeHistory") + + local options = recordingOptions or {} :: never + + local name = if options.name then `{r.command:formatFullId()}/{options.name}` else r.command:formatFullId() + local displayName = "Rocket: " + .. if options.displayName then `{r.command.schema.title} - {options.displayName}` else r.command.schema.title + + local success, id: string? = pcall(ChangeHistoryService.TryBeginRecording, ChangeHistoryService, name, displayName) + + if id == nil then + if not options.hideRobloxFailure then + local reason = if not success + then "Roblox failed to begin recording the action" + elseif ChangeHistoryService:IsRecordingInProgress() then "an undo-able action is still running" + elseif RunService:IsRunning() then "the game is running" + else "there was an unknown Roblox issue" + + r.log:warn("Failed to record changes for", displayName, "because", reason, `(recording name: {name})`) + end + + if not options.ignoreRobloxFailure then + return function(_operation: Enum.FinishRecordingOperation?) end + end + end + + local function finishRecording(operation: Enum.FinishRecordingOperation?) + if id then + local recordingId = id + id = nil + ChangeHistoryService:FinishRecording(recordingId, operation or Enum.FinishRecordingOperation.Commit) + end + end + + r.trove:Add(finishRecording :: any) + return finishRecording +end + +return recordChangeHistory diff --git a/modules/std/init.luau b/modules/std/init.luau new file mode 100644 index 0000000..d8b7aad --- /dev/null +++ b/modules/std/init.luau @@ -0,0 +1,15 @@ +local identity = require("@modules/std/functions/identity") +local noop = require("@modules/std/functions/noop") +local recordChangeHistory = require("@modules/std/history/recordChangeHistory") + +export type RecordChangeHistoryOptions = recordChangeHistory.RecordChangeHistoryOptions + +local std = table.freeze({ + recordChangeHistory = recordChangeHistory, + + identity = identity, + noop = noop, + singleton = identity :: (singleton: T | "") -> T, +}) + +return std diff --git a/modules/std/sorting/compareByTitle.luau b/modules/std/sorting/compareByTitle.luau new file mode 100644 index 0000000..e47f3bb --- /dev/null +++ b/modules/std/sorting/compareByTitle.luau @@ -0,0 +1,7 @@ +export type TitleLike = { title: string, [any]: any } + +local function compareByTitle(lhs: L & TitleLike, rhs: R & TitleLike) + return lhs.title < rhs.title +end + +return compareByTitle diff --git a/modules/std/vendor/.luaurc b/modules/std/vendor/.luaurc new file mode 100644 index 0000000..16c086d --- /dev/null +++ b/modules/std/vendor/.luaurc @@ -0,0 +1,6 @@ +{ + "languageMode": "nonstrict", + "lint": { + "*": false + } +} diff --git a/modules/std/vendor/fzy.luau b/modules/std/vendor/fzy.luau new file mode 100644 index 0000000..49809e8 --- /dev/null +++ b/modules/std/vendor/fzy.luau @@ -0,0 +1,274 @@ +-- The lua implementation of the fzy string matching algorithm + +local SCORE_GAP_LEADING = -0.005 +local SCORE_GAP_TRAILING = -0.005 +local SCORE_GAP_INNER = -0.01 +local SCORE_MATCH_CONSECUTIVE = 1.0 +local SCORE_MATCH_SLASH = 0.9 +local SCORE_MATCH_WORD = 0.8 +local SCORE_MATCH_CAPITAL = 0.7 +local SCORE_MATCH_DOT = 0.6 +local SCORE_MAX = math.huge +local SCORE_MIN = -math.huge +local MATCH_MAX_LENGTH = 1024 + +local fzy = {} + +-- Check if `needle` is a subsequence of the `haystack`. +-- +-- Usually called before `score` or `positions`. +-- +-- Args: +-- needle (string) +-- haystack (string) +-- case_sensitive (bool, optional): defaults to false +-- +-- Returns: +-- bool +function fzy.has_match(needle, haystack, case_sensitive) + if not case_sensitive then + needle = string.lower(needle) + haystack = string.lower(haystack) + end + + local j = 1 + for i = 1, string.len(needle) do + j = string.find(haystack, needle:sub(i, i), j, true) + if not j then + return false + else + j = j + 1 + end + end + + return true +end + +local function is_lower(c) + return c:match("%l") +end + +local function is_upper(c) + return c:match("%u") +end + +local function precompute_bonus(haystack) + local match_bonus = {} + + local last_char = "/" + for i = 1, string.len(haystack) do + local this_char = haystack:sub(i, i) + if last_char == "/" or last_char == "\\" then + match_bonus[i] = SCORE_MATCH_SLASH + elseif last_char == "-" or last_char == "_" or last_char == " " then + match_bonus[i] = SCORE_MATCH_WORD + elseif last_char == "." then + match_bonus[i] = SCORE_MATCH_DOT + elseif is_lower(last_char) and is_upper(this_char) then + match_bonus[i] = SCORE_MATCH_CAPITAL + else + match_bonus[i] = 0 + end + + last_char = this_char + end + + return match_bonus +end + +local function compute(needle, haystack, D, M, case_sensitive) + -- Note that the match bonuses must be computed before the arguments are + -- converted to lowercase, since there are bonuses for camelCase. + local match_bonus = precompute_bonus(haystack) + local n = string.len(needle) + local m = string.len(haystack) + + if not case_sensitive then + needle = string.lower(needle) + haystack = string.lower(haystack) + end + + -- Because lua only grants access to chars through substring extraction, + -- get all the characters from the haystack once now, to reuse below. + local haystack_chars = {} + for i = 1, m do + haystack_chars[i] = haystack:sub(i, i) + end + + for i = 1, n do + D[i] = {} + M[i] = {} + + local prev_score = SCORE_MIN + local gap_score = i == n and SCORE_GAP_TRAILING or SCORE_GAP_INNER + local needle_char = needle:sub(i, i) + + for j = 1, m do + if needle_char == haystack_chars[j] then + local score = SCORE_MIN + if i == 1 then + score = ((j - 1) * SCORE_GAP_LEADING) + match_bonus[j] + elseif j > 1 then + local a = M[i - 1][j - 1] + match_bonus[j] + local b = D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE + score = math.max(a, b) + end + D[i][j] = score + prev_score = math.max(score, prev_score + gap_score) + M[i][j] = prev_score + else + D[i][j] = SCORE_MIN + prev_score = prev_score + gap_score + M[i][j] = prev_score + end + end + end +end + +-- Compute a matching score. +-- +-- Args: +-- needle (string): must be a subequence of `haystack`, or the result is +-- undefined. +-- haystack (string) +-- case_sensitive (bool, optional): defaults to false +-- +-- Returns: +-- number: higher scores indicate better matches. See also `get_score_min` +-- and `get_score_max`. +function fzy.score(needle, haystack, case_sensitive) + local n = string.len(needle) + local m = string.len(haystack) + + if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > m then + return SCORE_MIN + elseif n == m then + return SCORE_MAX + else + local D = {} + local M = {} + compute(needle, haystack, D, M, case_sensitive) + return M[n][m] + end +end + +-- Compute the locations where fzy matches a string. +-- +-- Determine where each character of the `needle` is matched to the `haystack` +-- in the optimal match. +-- +-- Args: +-- needle (string): must be a subequence of `haystack`, or the result is +-- undefined. +-- haystack (string) +-- case_sensitive (bool, optional): defaults to false +-- +-- Returns: +-- {int,...}: indices, where `indices[n]` is the location of the `n`th +-- character of `needle` in `haystack`. +-- number: the same matching score returned by `score` +function fzy.positions(needle, haystack, case_sensitive) + local n = string.len(needle) + local m = string.len(haystack) + + if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > m then + return {}, SCORE_MIN + elseif n == m then + local consecutive = {} + for i = 1, n do + consecutive[i] = i + end + return consecutive, SCORE_MAX + end + + local D = {} + local M = {} + compute(needle, haystack, D, M, case_sensitive) + + local positions = {} + local match_required = false + local j = m + for i = n, 1, -1 do + while j >= 1 do + if D[i][j] ~= SCORE_MIN and (match_required or D[i][j] == M[i][j]) then + match_required = (i ~= 1) and (j ~= 1) and (M[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE) + positions[i] = j + j = j - 1 + break + else + j = j - 1 + end + end + end + + return positions, M[n][m] +end + +-- Apply `has_match` and `positions` to an array of haystacks. +-- +-- Args: +-- needle (string) +-- haystack ({string, ...}) +-- case_sensitive (bool, optional): defaults to false +-- +-- Returns: +-- {{idx, positions, score}, ...}: an array with one entry per matching line +-- in `haystacks`, each entry giving the index of the line in `haystacks` +-- as well as the equivalent to the return value of `positions` for that +-- line. +function fzy.filter(needle: string, haystacks: { string }, case_sensitive: boolean?): { { number } } + local result = {} + + for i, line in ipairs(haystacks) do + if fzy.has_match(needle, line, case_sensitive) then + local p, s = fzy.positions(needle, line, case_sensitive) + table.insert(result, { i, p, s }) + end + end + + return result +end + +-- The lowest value returned by `score`. +-- +-- In two special cases: +-- - an empty `needle`, or +-- - a `needle` or `haystack` larger than than `get_max_length`, +-- the `score` function will return this exact value, which can be used as a +-- sentinel. This is the lowest possible score. +function fzy.get_score_min() + return SCORE_MIN +end + +-- The score returned for exact matches. This is the highest possible score. +function fzy.get_score_max() + return SCORE_MAX +end + +-- The maximum size for which `fzy` will evaluate scores. +function fzy.get_max_length() + return MATCH_MAX_LENGTH +end + +-- The minimum score returned for normal matches. +-- +-- For matches that don't return `get_score_min`, their score will be greater +-- than than this value. +function fzy.get_score_floor() + return MATCH_MAX_LENGTH * SCORE_GAP_INNER +end + +-- The maximum score for non-exact matches. +-- +-- For matches that don't return `get_score_max`, their score will be less than +-- this value. +function fzy.get_score_ceiling() + return MATCH_MAX_LENGTH * SCORE_MATCH_CONSECUTIVE +end + +-- The name of the currently-running implmenetation, "lua" or "native". +function fzy.get_implementation_name() + return "lua" +end + +return fzy diff --git a/modules/std/vendor/repr.luau b/modules/std/vendor/repr.luau new file mode 100644 index 0000000..cc34063 --- /dev/null +++ b/modules/std/vendor/repr.luau @@ -0,0 +1,339 @@ +--!nocheck +--!nolint +--#selene: allow(unused_variable, shadowing) +--- repr - Version 1.2 +-- Ozzypig - ozzypig.com - http://twitter.com/Ozzypig +-- Check out this thread for more info: +-- https://devforum.roblox.com/t/repr-function-for-printing-tables/276575 +--[[ + +local repr = require(3148021300) + +local myTable = { + hello = "world", + score = 5, + isCool = true +} +print(repr(myTable)) --> {hello = "world", isCool = true, score = 5} + +]] + +local defaultSettings = { + pretty = false, + robloxFullName = false, + robloxProperFullName = true, + robloxClassName = true, + tabs = false, + semicolons = false, + spaces = 3, + sortKeys = true, +} + +-- lua keywords +local keywords = { + ["and"] = true, + ["break"] = true, + ["do"] = true, + ["else"] = true, + ["elseif"] = true, + ["end"] = true, + ["false"] = true, + ["for"] = true, + ["function"] = true, + ["if"] = true, + ["in"] = true, + ["local"] = true, + ["nil"] = true, + ["not"] = true, + ["or"] = true, + ["repeat"] = true, + ["return"] = true, + ["then"] = true, + ["true"] = true, + ["until"] = true, + ["while"] = true, +} + +local function isLuaIdentifier(str) + if type(str) ~= "string" then + return false + end + -- must be nonempty + if str:len() == 0 then + return false + end + -- can only contain a-z, A-Z, 0-9 and underscore + if str:find("[^%d%a_]") then + return false + end + -- cannot begin with digit + if tonumber(str:sub(1, 1)) then + return false + end + -- cannot be keyword + if keywords[str] then + return false + end + return true +end + +-- works like Instance:GetFullName(), but invalid Lua identifiers are fixed (e.g. workspace["The Dude"].Humanoid) +local function properFullName(object, usePeriod) + if object == nil or object == game then + return "" + end + + local s = object.Name + local usePeriod = true + if not isLuaIdentifier(s) then + s = ("[%q]"):format(s) + usePeriod = false + end + + if not object.Parent or object.Parent == game then + return s + else + return properFullName(object.Parent) .. (usePeriod and "." or "") .. s + end +end + +local depth = 0 +local shown +local INDENT +local reprSettings + +local function repr(value: any, reprSettings: any?) + reprSettings = reprSettings or defaultSettings + INDENT = (" "):rep(reprSettings.spaces or defaultSettings.spaces) + if reprSettings.tabs then + INDENT = "\t" + end + + local v = value --args[1] + local tabs = INDENT:rep(depth) + + if depth == 0 then + shown = {} + end + if type(v) == "string" then + return ("%q"):format(v) + elseif type(v) == "number" then + if v == math.huge then + return "math.huge" + end + if v == -math.huge then + return "-math.huge" + end + return tonumber(v) + elseif type(v) == "boolean" then + return tostring(v) + elseif type(v) == "nil" then + return "nil" + elseif type(v) == "table" and type(v.__tostring) == "function" then + return tostring(v.__tostring(v)) + elseif type(v) == "table" and getmetatable(v) and type(getmetatable(v).__tostring) == "function" then + return tostring(getmetatable(v).__tostring(v)) + elseif type(v) == "table" then + if shown[v] then + return "{CYCLIC}" + end + shown[v] = true + local str = "{" .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or "") + local isArray = true + for k, v in pairs(v) do + if type(k) ~= "number" then + isArray = false + break + end + end + if isArray then + for i = 1, #v do + if i ~= 1 then + str = str + .. (reprSettings.semicolons and ";" or ",") + .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or " ") + end + depth = depth + 1 + str = str .. repr(v[i], reprSettings) + depth = depth - 1 + end + else + local keyOrder = {} + local keyValueStrings = {} + for k, v in pairs(v) do + depth = depth + 1 + local kStr = isLuaIdentifier(k) and k or ("[" .. repr(k, reprSettings) .. "]") + local vStr = repr(v, reprSettings) + --[[str = str .. ("%s = %s"):format( + isLuaIdentifier(k) and k or ("[" .. repr(k, reprSettings) .. "]"), + repr(v, reprSettings) + )]] + table.insert(keyOrder, kStr) + keyValueStrings[kStr] = vStr + depth = depth - 1 + end + if reprSettings.sortKeys then + table.sort(keyOrder) + end + local first = true + for _, kStr in pairs(keyOrder) do + if not first then + str = str + .. (reprSettings.semicolons and ";" or ",") + .. (reprSettings.pretty and ("\n" .. INDENT .. tabs) or " ") + end + str = str .. ("%s = %s"):format(kStr, keyValueStrings[kStr]) + first = false + end + end + shown[v] = false + if reprSettings.pretty then + str = str .. "\n" .. tabs + end + str = str .. "}" + return str + elseif typeof then + -- Check Roblox types + if typeof(v) == "Instance" then + return ( + reprSettings.robloxFullName + and (reprSettings.robloxProperFullName and properFullName(v) or v:GetFullName()) + or v.Name + ) .. (reprSettings.robloxClassName and ((" (%s)"):format(v.ClassName)) or "") + elseif typeof(v) == "Axes" then + local s = {} + if v.X then + table.insert(s, repr(Enum.Axis.X, reprSettings)) + end + if v.Y then + table.insert(s, repr(Enum.Axis.Y, reprSettings)) + end + if v.Z then + table.insert(s, repr(Enum.Axis.Z, reprSettings)) + end + return ("Axes.new(%s)"):format(table.concat(s, ", ")) + elseif typeof(v) == "BrickColor" then + return ("BrickColor.new(%q)"):format(v.Name) + elseif typeof(v) == "CFrame" then + return ("CFrame.new(%s)"):format(table.concat({ v:GetComponents() }, ", ")) + elseif typeof(v) == "Color3" then + return ("Color3.new(%d, %d, %d)"):format(v.r, v.g, v.b) + elseif typeof(v) == "ColorSequence" then + if #v.Keypoints > 2 then + return ("ColorSequence.new(%s)"):format(repr(v.Keypoints, reprSettings)) + else + if v.Keypoints[1].Value == v.Keypoints[2].Value then + return ("ColorSequence.new(%s)"):format(repr(v.Keypoints[1].Value, reprSettings)) + else + return ("ColorSequence.new(%s, %s)"):format( + repr(v.Keypoints[1].Value, reprSettings), + repr(v.Keypoints[2].Value, reprSettings) + ) + end + end + elseif typeof(v) == "ColorSequenceKeypoint" then + return ("ColorSequenceKeypoint.new(%d, %s)"):format(v.Time, repr(v.Value, reprSettings)) + elseif typeof(v) == "DockWidgetPluginGuiInfo" then + return ("DockWidgetPluginGuiInfo.new(%s, %s, %s, %s, %s, %s, %s)"):format( + repr(v.InitialDockState, reprSettings), + repr(v.InitialEnabled, reprSettings), + repr(v.InitialEnabledShouldOverrideRestore, reprSettings), + repr(v.FloatingXSize, reprSettings), + repr(v.FloatingYSize, reprSettings), + repr(v.MinWidth, reprSettings), + repr(v.MinHeight, reprSettings) + ) + elseif typeof(v) == "Enums" then + return "Enums" + elseif typeof(v) == "Enum" then + return ("Enum.%s"):format(tostring(v)) + elseif typeof(v) == "EnumItem" then + return ("Enum.%s.%s"):format(tostring(v.EnumType), v.Name) + elseif typeof(v) == "Faces" then + local s = {} + for _, enumItem in pairs(Enum.NormalId:GetEnumItems()) do + if v[enumItem.Name] then + table.insert(s, repr(enumItem, reprSettings)) + end + end + return ("Faces.new(%s)"):format(table.concat(s, ", ")) + elseif typeof(v) == "NumberRange" then + if v.Min == v.Max then + return ("NumberRange.new(%d)"):format(v.Min) + else + return ("NumberRange.new(%d, %d)"):format(v.Min, v.Max) + end + elseif typeof(v) == "NumberSequence" then + if #v.Keypoints > 2 then + return ("NumberSequence.new(%s)"):format(repr(v.Keypoints, reprSettings)) + else + if v.Keypoints[1].Value == v.Keypoints[2].Value then + return ("NumberSequence.new(%d)"):format(v.Keypoints[1].Value) + else + return ("NumberSequence.new(%d, %d)"):format(v.Keypoints[1].Value, v.Keypoints[2].Value) + end + end + elseif typeof(v) == "NumberSequenceKeypoint" then + if v.Envelope ~= 0 then + return ("NumberSequenceKeypoint.new(%d, %d, %d)"):format(v.Time, v.Value, v.Envelope) + else + return ("NumberSequenceKeypoint.new(%d, %d)"):format(v.Time, v.Value) + end + elseif typeof(v) == "PathWaypoint" then + return ("PathWaypoint.new(%s, %s)"):format(repr(v.Position, reprSettings), repr(v.Action, reprSettings)) + elseif typeof(v) == "PhysicalProperties" then + return ("PhysicalProperties.new(%d, %d, %d, %d, %d)"):format( + v.Density, + v.Friction, + v.Elasticity, + v.FrictionWeight, + v.ElasticityWeight + ) + elseif typeof(v) == "Random" then + return "" + elseif typeof(v) == "Ray" then + return ("Ray.new(%s, %s)"):format(repr(v.Origin, reprSettings), repr(v.Direction, reprSettings)) + elseif typeof(v) == "RBXScriptConnection" then + return "" + elseif typeof(v) == "RBXScriptSignal" then + return "" + elseif typeof(v) == "Rect" then + return ("Rect.new(%d, %d, %d, %d)"):format(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y) + elseif typeof(v) == "Region3" then + local min = v.CFrame.p + v.Size * -0.5 + local max = v.CFrame.p + v.Size * 0.5 + return ("Region3.new(%s, %s)"):format(repr(min, reprSettings), repr(max, reprSettings)) + elseif typeof(v) == "Region3int16" then + return ("Region3int16.new(%s, %s)"):format(repr(v.Min, reprSettings), repr(v.Max, reprSettings)) + elseif typeof(v) == "TweenInfo" then + return ("TweenInfo.new(%d, %s, %s, %d, %s, %d)"):format( + v.Time, + repr(v.EasingStyle, reprSettings), + repr(v.EasingDirection, reprSettings), + v.RepeatCount, + repr(v.Reverses, reprSettings), + v.DelayTime + ) + elseif typeof(v) == "UDim" then + return ("UDim.new(%d, %d)"):format(v.Scale, v.Offset) + elseif typeof(v) == "UDim2" then + return ("UDim2.new(%d, %d, %d, %d)"):format(v.X.Scale, v.X.Offset, v.Y.Scale, v.Y.Offset) + elseif typeof(v) == "Vector2" then + return ("Vector2.new(%d, %d)"):format(v.X, v.Y) + elseif typeof(v) == "Vector2int16" then + return ("Vector2int16.new(%d, %d)"):format(v.X, v.Y) + elseif typeof(v) == "Vector3" then + return ("Vector3.new(%d, %d, %d)"):format(v.X, v.Y, v.Z) + elseif typeof(v) == "Vector3int16" then + return ("Vector3int16.new(%d, %d, %d)"):format(v.X, v.Y, v.Z) + elseif typeof(v) == "DateTime" then + return ("DateTime.fromIsoDate(%q)"):format(v:ToIsoDate()) + else + return "" + end + else + return "<" .. type(v) .. ">" + end +end + +return repr diff --git a/modules/std/vendor/typeforge.luau b/modules/std/vendor/typeforge.luau new file mode 100644 index 0000000..2d36a34 --- /dev/null +++ b/modules/std/vendor/typeforge.luau @@ -0,0 +1,2587 @@ +--!strict +--!nolint LocalShadow +--#selene: allow(undefined_variable, shadowing, unused_variable) + +--> Helpers ---------------------------------------------------------------------------- +type function handle_negation(input: type, callback: (input: type, input_tag: string) -> T) + local input_tag = input.tag + + if input_tag == "negation" then + local input_inner = input:inner() + return types.negationof(callback(input_inner, input_inner.tag)) + else + return callback(input, input_tag) + end +end + +type function innermost_tag(input: type) + local input_tag = input.tag + return if input_tag == "negation" then input:inner().tag else input_tag +end + +type function assert_is_msg(input: type, input_type: string, label: string, is: string) + local error_msg_start = `\n'{label}' should be of type {is}` + + local error_msg_end = if is_primitive(input_type) + then `but is instead of type '{input_type}'!` + else `but is instead of type '{input_type}': {stringify_preview(input)}` + + return `{error_msg_start} {error_msg_end}` +end + +-- Asserts if the input is of the required types - with a nice formatted error message. +type function assert_is(input: type, label: string, input_tag: string, ...: string) + local input_type = if input_tag == "singleton" then type(input:value()) else input_tag + + if select("#", ...) == 1 then + local is = ... + if input_type ~= is then + assert_is_msg(input, input_type, label, `'{is}'`) + end + else + local allowed_types = table.pack(...) + + local assert_failed = false + for _, allowed_type in allowed_types do + if input_type ~= allowed_type then + assert_failed = true + break + end + end + + if assert_failed then + local allowed_types_len = #allowed_types + local allowed_types_str = `'{allowed_types[1]}'` + for idx = 2, allowed_types_len - 1 do + allowed_types_str ..= `, '{allowed_types[idx]}'` + end + allowed_types_str ..= ` or '{allowed_types[allowed_types_len]}'` + + error(assert_is_msg(input, label, input_type, allowed_types_str)) + end + end +end + +type function is_primitive(input_tag: string) + return input_tag == "unknown" + or input_tag == "never" + or input_tag == "any" + or input_tag == "boolean" + or input_tag == "number" + or input_tag == "string" +end + +type function unions_and_intersections_flatten(input: typeof(types.unionof()) | typeof(types.intersectionof())) + local input_tag = input.tag + + if input_tag == "union" or input_tag == "intersection" then + local components = input:components() + + for idx = #components, 1, -1 do + local component = components[idx] + local component_tag = component.tag + if component_tag ~= "union" and component_tag ~= "intersection" then + continue + end + + local component = unions_and_intersections_flatten(component) + if component.tag == input_tag then + local sub_components = component:components() + + components[idx] = sub_components[1] + for idx = 2, #sub_components do + table.insert(components :: any, sub_components[idx]) + end + else + components[idx] = component + end + end + + return ( + if input_tag == "union" then union_from_components(components) else intersection_from_components(components) + ) or types.never + else + return input + end +end + +-- The equality operator `==` is really strict with unions and intersections, +-- where the order of their components needs to be the same. +type function sort_type(input: type) + local input_tag = input.tag + + if input_tag == "union" or input_tag == "intersection" then + local components = input:components() + + table.sort(components, function(a: type, b: type): boolean + local a_tag, b_tag = a.tag, b.tag + + return if a_tag ~= b_tag + then a_tag < b_tag + elseif a_tag == "table" or a_tag == "union" or a_tag == "intersection" then stringify(a) > stringify( + b + ) + elseif a_tag ~= "singleton" then false + else (a:value() :: any) < (b:value() :: any) + end) + + for idx, component in components do + components[idx] = sort_type(component) + end + + return (if input_tag == "union" then union_from_components else intersection_from_components)(components) + elseif input_tag == "function" then + local params = input:parameters() + local params_head, params_tail = params.head, params.tail + + if params_head then + for idx, value in params_head do + params_head[idx] = sort_type(value) + end + end + + if params_tail then + params_tail = sort_type(params_tail) + end + + local returns = input:returns() + local returns_head, returns_tail = returns.head, returns.tail + + if returns_head then + for idx, value in returns_head do + returns_head[idx] = sort_type(value) + end + end + + if returns_tail then + returns_tail = sort_type(returns_tail) + end + + return types.newfunction( + { head = params_head, tail = params_tail }, + { head = returns_head, tail = returns_tail } + ) + elseif input_tag == "table" then + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + + if value_read == value_write then + input:setproperty(key, sort_type(value_read)) + else + if value_read then + input:setreadproperty(key, sort_type(value_read)) + end + if value_write then + input:setwriteproperty(key, sort_type(value_write)) + end + end + end + + -- TODO: add support for read and write indexers when available. + local indexer = input:indexer() + if indexer then + input:setindexer(sort_type(indexer.index), sort_type(indexer.readresult)) + end + + return input + elseif input_tag == "negation" then + return types.negationof(sort_type(input:inner())) + else + return input + end +end +---------------------------------------------------------------------------------------- + +--> Stringify Helpers ------------------------------------------------------------------ +type function stringify_preview(input: type) + return `\n> {string.gsub(stringify(input), "\n", "\n> ")}` +end + +type function stringify_table( + input: typeof(types.singleton(nil)), + nestedness: number, + pre_padding: string?, + post_padding: string? +) + local properties = input:properties() + local indexer = input:indexer() + + if table_value_is_empty(properties, indexer) then + return "{ }" + end + + local next_nestedness = nestedness + 1 + + local outer_padding = string.rep(" ", nestedness - 1) + local padding = outer_padding .. " " + + local stringified = `{pre_padding or outer_padding}\{` + + -- Sorts the table keys so its stringified representation + -- to ensure stringification is deterministic. + local keys = {} + for key in properties do + table.insert(keys :: any, key) + end + table.sort(keys :: any, function(a, b) + return tostring(a) > tostring(b) + end) + + for _, key in keys :: any do + local value = properties[key] + + local key_value = if key.tag == "singleton" then key:value() else input + + local stringified_key = if type(key_value) == "string" + then key_value + else `[{stringify_main(key, next_nestedness, "", padding)}]` + + local value_read, value_write = value.read, value.write + + if value_read == value_write then + stringified ..= `\n{padding}{stringified_key}: {stringify_main(value_read, next_nestedness)}` + else + if value_read then + stringified ..= `\n{padding}read {stringified_key}: {stringify_main(value_read, next_nestedness)}` + end + if value_write then + stringified ..= `\n{padding}write {stringified_key}: {stringify_main(value_write, next_nestedness)}` + end + end + end + + -- TODO: add support for read and write indexers when available. + if indexer then + local indexer_index = indexer.index + local indexer_index_value = if indexer_index.tag == "singleton" then indexer_index:value() else input + + local stringified_indexer_index = if type(indexer_index_value) == "string" + then indexer_index_value + else `[{stringify_main(indexer_index, next_nestedness, "", padding)}]` + + stringified ..= `\n{padding}{stringified_indexer_index}: {stringify_main(indexer.readresult, next_nestedness)}` + end + + return stringified .. `\n{post_padding or outer_padding}}` +end + +type function stringify_components(components: { [number]: type }, concatenator: string) + for idx, component in components do + local component_tag = component.tag + + if component_tag == "union" then + components[idx] = `({stringify_components(component:components(), " | ")})` :: any + elseif component_tag == "intersection" then + components[idx] = `({stringify_components(component:components(), " & ")})` :: any + else + components[idx] = stringify_main(component, 1) + end + end + + return table.concat(components, concatenator) +end + +type function stringify_function(input: typeof(types.newfunction())) + local params = input:parameters() + local params_head, params_tail = params.head, params.tail + + local stringified_params: string + if params_head then + stringified_params = "" + + local params_head_len = #params_head + for idx = 1, params_head_len - 1 do + stringified_params ..= `{stringify(params_head[idx])}, ` + end + stringified_params ..= `{stringify(params_head[params_head_len])}` + + if params_tail then + local params_tail_tag = params_tail.tag + if params_tail_tag == "union" or params_tail_tag == "intersection" then + stringified_params ..= `, ...({stringify(params_tail)}))` + else + stringified_params ..= `, ...{stringify(params_tail)})` + end + end + elseif params_tail then + local params_tail_tag = params_tail.tag + if params_tail_tag == "union" or params_tail_tag == "intersection" then + stringified_params = `...({stringify(params_tail)})` + else + stringified_params = `...{stringify(params_tail)}` + end + end + + local returns = input:returns() + local returns_head, returns_tail = returns.head, returns.tail + + local returns_head_len: number + local stringified_returns: string + if returns_head then + stringified_returns = "" + + returns_head_len = #returns_head + for idx = 1, returns_head_len - 1 do + stringified_returns ..= `{stringify(returns_head[idx])}, ` + end + stringified_returns ..= `{stringify(returns_head[returns_head_len])}` + + if returns_tail then + local returns_tail_tag = returns_tail.tag + if returns_tail_tag == "union" or returns_tail_tag == "intersection" then + stringified_returns ..= `, ...({stringify(returns_tail)}))` + else + stringified_returns ..= `, ...{stringify(returns_tail)})` + end + end + elseif returns_tail then + local returns_tail_tag = returns_tail.tag + if returns_tail_tag == "union" or returns_tail_tag == "intersection" then + stringified_returns = `...({stringify(returns_tail)})` + else + stringified_returns = `...{stringify(returns_tail)}` + end + end + + local stringified_returns = if returns_tail or (returns_head and returns_head_len >= 2) + then `({stringified_returns})` + else stringified_returns + + return `({stringified_params}) -> {stringified_returns}` +end + +type function stringify_main(input: type, nestedness: number, pre_padding: string?, post_padding: string?) + local input_tag = input.tag + + if input_tag == "negation" then + local input_inner = input:inner() + local input_inner_tag = input_inner.tag + return if (input_inner_tag == "union" or input_inner_tag == "intersection") + then `~({stringify_main(input:inner(), nestedness)})` + else `~{stringify_main(input:inner(), nestedness)}` + end + + if input_tag == "union" then + return stringify_components(input:components(), " | ") + elseif input_tag == "intersection" then + return stringify_components(input:components(), " & ") + elseif input_tag == "table" then + return stringify_table(input, nestedness, pre_padding, post_padding) + elseif input_tag == "function" then + return stringify_function(input) + elseif input_tag == "unknown" then + return "unknown" + elseif input_tag == "never" then + return "never" + elseif input_tag == "any" then + return "any" + elseif input_tag == "boolean" then + return "boolean" + elseif input_tag == "number" then + return "number" + elseif input_tag == "string" then + return "string" + end + + local input = if input_tag == "singleton" then input:value() else input + local input_type = type(input) + + return if input_type == "string" then `"{input}"` else tostring(input) +end + +type function stringify(input: type) + return stringify_main(input, 1) +end +---------------------------------------------------------------------------------------- + +--> Hashset Helpers -------------------------------------------------------------------- +type function hashset_can_insert(input_tag: string) + return input_tag == "singleton" +end + +type function hashset_insert( + hashset: typeof(types.newtable()), + non_hashsettable: { [number]: type }, + input: type, + input_tag: string +) + if hashset_can_insert(input_tag) then + hashset:setproperty(input, types.never) + else + table.insert(non_hashsettable, input) + end +end + +type function hashset_has_type( + hashset: typeof(types.newtable()), + non_hashsettable: { [number]: type }, + input: type, + input_tag: string +) + if hashset_can_insert(input_tag) then + return if hashset:readproperty(input) then true else false + else + for _, component in non_hashsettable do + if input == component then + return true + end + end + + return false + end +end + +type function hashset_from_components(input: { [number]: type }) + local hashset = types.newtable() + + local input_len = #input + + for idx = input_len, 1, -1 do + local component = input[idx] + + if hashset_can_insert(component.tag) then + hashset:setproperty(component, types.never) + input_len = table_value_swap_remove(input, input_len, idx) + end + end + + -- The input table should now only contain the + -- components that could not be inserted into the hashset. + return hashset, input +end +---------------------------------------------------------------------------------------- + +--> Components Helpers ----------------------------------------------------------------- +type function components_filter( + input: { [number]: type }, + as: "union" | "intersection", + as_builder: ( + components: { [number]: type } + ) -> typeof(types.unionof()) | typeof(types.intersectionof()), + callback: (component: type) -> boolean +) + local input_len = #input + + for idx = input_len, 1, -1 do + local component = input[idx] + + if component.tag == as then + local filtered = as_builder(components_filter(component:components(), as, as_builder, callback)) + + if filtered == nil then + input_len = table_value_swap_remove(input, input_len, idx) + else + input[idx] = filtered + end + else + if not callback(component) then + continue + end + + input_len = table_value_swap_remove(input, input_len, idx) + end + end + + return input +end + +type function components_flatten( + input: { [number]: type }, + as: "union" | "intersection", + as_builder: ( + components: { [number]: type } + ) -> typeof(types.unionof()) | typeof(types.intersectionof()) +) + for idx = #input, 1, -1 do + local component = input[idx] + local component_tag = component.tag + + if component_tag == "negation" then + component = component:inner() + if component.tag ~= as then + continue + end + + local component = types.negationof(as_builder(components_flatten(component:components(), as))) + input[idx] = component + elseif component_tag == as then + local component = components_flatten(component:components(), as) + input[idx] = component[1] + for idx = 2, #component do + table.insert(input, component[idx]) + end + end + end + + return input +end + +type function components_map(input: { [number]: type }, callback: (value: type, ...any) -> any, ...: any) + for idx, value in input do + input[idx] = callback(value, ...) + end + return input +end + +type function components_clean( + input: { [number]: type }, + as: "union" | "intersection", + as_builder: ( + components: { [number]: type } + ) -> typeof(types.unionof()) | typeof(types.intersectionof()), + hashset: typeof(types.newtable()), + non_hashsettable: { [number]: type } +) + local input_len = #input + for idx = input_len, 1, -1 do + local component = input[idx] + local component_tag = component.tag + + if component_tag == as then + local cleaned = + as_builder(components_clean(component:components(), as, as_builder, hashset, non_hashsettable)) + + if cleaned == nil then + input_len = table_value_swap_remove(input, input_len, idx) + else + input[idx] = cleaned + end + + continue + elseif component_tag == "negation" then + local inner = component:inner() + + if inner.tag == as then + local cleaned = + as_builder(components_clean(inner:components(), as, as_builder, hashset, non_hashsettable)) + + if cleaned == nil then + input_len = table_value_swap_remove(input, input_len, idx) + else + input[idx] = types.negationof(cleaned) + end + + continue + end + end + + if hashset_has_type(hashset, non_hashsettable, component, component_tag) then + input_len = table_value_swap_remove(input, input_len, idx) + else + hashset_insert(hashset, non_hashsettable, component, component_tag) + end + end + + return input +end + +type function components_clean_unions_and_intersections( + input: { [number]: type }, + hashset: typeof(types.newtable()), + non_hashsettable: { [number]: type }, + + input_tag: string, + input_tag_other: string, + + input_builder: ( + components: { [number]: type } + ) -> typeof(types.unionof()) | typeof(types.intersectionof()), + input_builder_other: ( + components: { [number]: type } + ) -> typeof(types.unionof()) | typeof(types.intersectionof()) +) + local input_len = #input + for idx = input_len, 1, -1 do + local component = input[idx] + local component_tag = component.tag + + if component_tag == input_tag then + local cleaned = input_builder( + components_clean_unions_and_intersections( + component:components(), + hashset, + non_hashsettable, + input_tag, + input_tag_other, + input_builder, + input_builder_other + ) + ) + + if cleaned == nil then + input_len = table_value_swap_remove(input, input_len, idx) + else + input[idx] = cleaned + end + elseif component_tag == input_tag_other then + local cleaned = input_builder_other( + components_clean_unions_and_intersections( + component:components(), + types.newtable(), + {}, + input_tag_other, + input_tag, + input_builder_other, + input_builder + ) + ) + + if cleaned == nil then + input_len = table_value_swap_remove(input, input_len, idx) + else + input[idx] = cleaned + end + else + if hashset_has_type(hashset, non_hashsettable, component, component_tag) then + input_len = table_value_swap_remove(input, input_len, idx) + else + hashset_insert(hashset, non_hashsettable, component, component_tag) + end + end + end + + return input +end + +type function components_find( + input: { [number]: type }, + as: "union" | "intersection", + callback: (component: type, component_tag: string) -> boolean +): boolean + for _, component in input do + local component_tag = component.tag + + if component_tag == as then + if components_find(component:components(), as, callback) then + return true + end + elseif callback(component, component_tag) then + return true + end + end + + return false +end + +type function components_find_type( + input: { [number]: type }, + as: "union" | "intersection", + tag_to_find: string +): boolean + return components_find(input, as, function(_, tag: string) + return tag == tag_to_find + end) +end +---------------------------------------------------------------------------------------- + +--> Union Helpers ---------------------------------------------------------------------- +type function union_from_components(input: { [number]: type }) + local input_len = #input + return if input_len == 0 then nil elseif input_len == 1 then input[1] else types.unionof(table.unpack(input)) +end + +type function union_filter(input: type, input_tag: string, callback: (component: type) -> boolean) + if input_tag == "union" then + return union_from_components(components_filter(input:components(), "union", union_from_components, callback)) + else + return if callback(input) then nil else input + end +end +---------------------------------------------------------------------------------------- + +--> Intersection Helpers --------------------------------------------------------------- +type function intersection_from_components(input: { [number]: type }) + local input_len = #input + return if input_len == 0 then nil elseif input_len == 1 then input[1] else types.intersectionof(table.unpack(input)) +end + +type function intersection_filter(input: type, input_tag: string, callback: (component: type) -> boolean) + if input_tag == "intersection" then + return intersection_from_components( + components_filter(input:components(), "intersection", intersection_from_components, callback) + ) + else + return if callback(input) then nil else input + end +end +---------------------------------------------------------------------------------------- + +--> Table Helpers ---------------------------------------------------------------------- +type function table_value_swap_remove(input: { [any]: any }, input_len: number, idx: number) + -- Last item, we can just remove from + -- the end of the input table. + if idx == input_len then + table.remove(input, idx) + + -- Swap remove optimisation. + else + input[idx] = table.remove(input) + end + + return input_len - 1 +end + +type function table_value_is_empty(properties: { [any]: any }, indexer: type) + if indexer then + return false + end + + for _ in properties do + return false + end + return true +end + +type function table_flatten_with( + input: typeof(types.newtable()), + with: typeof(types.newtable()), + can_add_read_indexer: boolean, + can_add_write_indexer: boolean +) + for key, value in with:properties() do + local can_add_read_property = not input:readproperty(key) + local can_add_write_property = not input:writeproperty(key) + + if can_add_read_property and can_add_write_property then + local value_read, value_write = value.read, value.write + + if value_read == value_write then + input:setproperty(key, value_read) + else + if value_read then + input:setreadproperty(key, value_read) + end + + if value_write then + input:setwriteproperty(key, value_write) + end + end + else + if can_add_read_property then + local value_read = value.read + if value_read then + input:setreadproperty(key, value_read) + end + end + + if can_add_write_property then + local value_write = value.write + if value_write then + input:setreadproperty(key, value_write) + end + end + end + end + + local indexer = with:indexer() + if indexer then + if can_add_read_indexer and can_add_write_indexer then + local indexer_read, indexer_write = indexer.readresult, indexer.writeresult + + if indexer_read == indexer_write then + input:setindexer(indexer.index, indexer_read) + else + if indexer_read and indexer_write then + local indexer_index = indexer.index + input:setreadindexer(indexer_index, indexer_read) + input:setwriteindexer(indexer_index, indexer_write) + can_add_read_indexer, can_add_write_indexer = false, false + elseif indexer_read then + input:setreadindexer(indexer.index, indexer_read) + can_add_read_indexer = false + elseif indexer_write then + input:setwriteindexer(indexer.index, indexer_write) + can_add_write_indexer = false + end + end + else + if can_add_read_indexer then + local indexer_read = indexer.readresult + if indexer_read then + input:setreadindexer(indexer.index, indexer_read) + can_add_read_indexer = false + end + elseif can_add_write_indexer then + local indexer_write = indexer.writeresult + if indexer_write then + input:setwriteindexer(indexer.index, indexer_write) + can_add_write_indexer = false + end + end + end + end + + return can_add_read_indexer, can_add_write_indexer +end + +-- Currently there is no way to remove an indexer from +-- a table type. So we resort to a hacky workaround of +-- creating a new table type and copying all information +-- from the input table type except the indexer. +type function table_remove_indexer_hacky_fix(input: typeof(types.newtable())) + local output = types.newtable() + + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + if value_read == value_write then + output:setproperty(key, value_read) + else + output:setreadproperty(key, value_read) + output:setwriteproperty(key, value_write) + end + end + + return output +end + +-- Currently there is no way to remove a read indexer from +-- a table type. So we resort to a hacky workaround of +-- creating a new table type and copying all information +-- from the input table type except the read indexer. +type function table_remove_read_indexer_hacky_fix(input: typeof(types.newtable())) + local output = types.newtable() + + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + if value_read == value_write then + output:setproperty(key, value_read) + else + output:setreadproperty(key, value_read) + output:setwriteproperty(key, value_write) + end + end + + local indexer = input:indexer() + if indexer then + local indexer_write = indexer.writeresult + if indexer_write then + output:setwriteindexer(indexer.index, indexer_write) + end + end + + return output +end + +-- Currently there is no way to remove a write indexer from +-- a table type. So we resort to a hacky workaround of +-- creating a new table type and copying all information +-- from the input table type except the write indexer. +type function table_remove_write_indexer_hacky_fix(input: typeof(types.newtable())) + local output = types.newtable() + + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + if value_read == value_write then + output:setproperty(key, value_read) + else + output:setreadproperty(key, value_read) + output:setwriteproperty(key, value_write) + end + end + + local indexer = input:indexer() + if indexer then + local indexer_read = indexer.readresult + if indexer_read then + output:setreadindexer(indexer.index, indexer_read) + end + end + + return output +end + +type function table_set_indexer_or_remove(input: typeof(types.newtable()), index: type, result: type?) + if not result then + return table_remove_indexer_hacky_fix(input) + else + input:setindexer(index, result) + end + return input +end + +type function table_set_read_indexer_or_remove(input: typeof(types.newtable()), index: type, result: type?) + if not result then + return table_remove_read_indexer_hacky_fix(input) + else + input:setreadindexer(index, result) + end + return input +end + +type function table_set_write_indexer_or_remove(input: typeof(types.newtable()), index: type, result: type?) + if not result then + return table_remove_write_indexer_hacky_fix(input) + else + input:setwriteindexer(index, result) + end + return input +end + +type function table_filter_process_value( + input: typeof(types.newtable()), + key: type, + value: type, + value_callback: (value: type) -> boolean, + setter_method: (input: typeof(types.newtable()), key: type, value: type?) -> nil +) + local value_tag = (value :: type).tag + if value_tag == "union" then + local value = + union_from_components(components_filter(value:components(), "union", union_from_components, value_callback)) + setter_method(input, key, value) + elseif value_tag == "intersection" then + local value = intersection_from_components( + components_filter(value:components(), "intersection", intersection_from_components, value_callback) + ) + setter_method(input, key, value) + elseif value_callback(value :: type) then + setter_method(input, key, nil) + end +end + +type function table_filter_process_indexer_result( + input: typeof(types.newtable()), + key: type, + value: type, + value_callback: (value: type) -> boolean, + setter_method: (input: typeof(types.newtable()), key: type, value: type?) -> nil, + key_updated: boolean +) + local value_tag = (value :: type).tag + if value_tag == "union" then + local value = + union_from_components(components_filter(value:components(), "union", union_from_components, value_callback)) + -- We need to set the input here in case a new table is + -- returned as part of hacky table indexer removal. + input = setter_method(input, key, value) or input + elseif value_tag == "intersection" then + local value = intersection_from_components( + components_filter(value:components(), "intersection", intersection_from_components, value_callback) + ) + -- We need to set the input here in case a new table is + -- returned as part of hacky table indexer removal. + input = setter_method(input, key, value) or input + elseif value_callback(value :: type) then + -- We need to set the input here in case a new table is + -- returned as part of hacky table indexer removal. + input = setter_method(input, key, nil) or input + elseif key_updated then + -- We need to set the input here in case a new table is + -- returned as part of hacky table indexer removal. + input = setter_method(input, key, value) or input + end + + -- We need to return the input here in case a new table is + -- returned as part of hacky table indexer removal. + return input +end + +type function table_filter_by_keys(input: typeof(types.newtable()), callback: (input: type) -> boolean) + for key, value in input:properties() do + if callback(key) then + input:setproperty(key, nil) + continue + end + end + + local indexer = input:indexer() + if indexer then + local indexer_index = indexer.index + + local indexer_index_tag = (indexer_index :: type).tag + if indexer_index_tag == "union" then + local filtered_indexer_index = union_from_components( + components_filter(indexer_index:components(), "union", union_from_components, callback) + ) + + if filtered_indexer_index == nil then + return table_remove_indexer_hacky_fix(input) + else + -- TODO: add support for read and write indexers when available. + input:setindexer(filtered_indexer_index, indexer.readresult) + --input:setreadindexer(filtered_indexer_index, indexer.readresult) + --input:setwriteindexer(filtered_indexer_index, indexer.writeresult) + end + elseif indexer_index_tag == "intersection" then + local filtered_indexer_index = intersection_from_components( + components_filter(indexer_index:components(), "intersection", intersection_from_components, callback) + ) + + if filtered_indexer_index == nil then + return table_remove_indexer_hacky_fix(input) + else + input:setreadindexer(filtered_indexer_index, indexer.readresult) + input:setwriteindexer(filtered_indexer_index, indexer.writeresult) + end + end + end + + return input +end + +type function table_filter_by_values(input: typeof(types.newtable()), callback: (key: type) -> boolean) + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + -- If the read and write values are identical then we only + -- need to perform the filter check on one of them. + if value_read == value_write then + table_filter_process_value(input, key, value_read, callback, input.setproperty) + else + table_filter_process_value(input, key, value_read, callback, input.setreadproperty) + table_filter_process_value(input, key, value_write, callback, input.setwriteproperty) + end + end + + local indexer = input:indexer() + if indexer then + local indexer_index = indexer.index + + local indexer_read, indexer_write = indexer.readresult, indexer.writeresult + + -- If the read and write values are identical then we only + -- need to perform the filter check on one of them. + if indexer_read == indexer_write then + input = table_filter_process_indexer_result( + input, + indexer_index, + indexer_read, + callback, + table_set_indexer_or_remove, + false + ) + else + input = table_filter_process_indexer_result( + input, + indexer_index, + indexer_read, + callback, + table_set_read_indexer_or_remove, + false + ) + input = table_filter_process_indexer_result( + input, + indexer_index, + indexer_write, + callback, + table_set_write_indexer_or_remove, + false + ) + end + end + + return input +end + +type function table_map_values_deep(input: typeof(types.newtable()), callback: (input: type) -> type?) + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + + if value_read == value_write then + input:setproperty( + key, + table_map_inner_components(callback(value_read :: type), table_map_values_deep, callback) + ) + else + if value_read then + input:setreadproperty( + key, + table_map_inner_components(callback(value_read), table_map_values_deep, callback) + ) + end + + if value_write then + input:setwriteproperty( + key, + table_map_inner_components(callback(value_write), table_map_values_deep, callback) + ) + end + end + end + + local indexer = input:indexer() + if indexer then + local indexer_index = indexer.index + local indexer_read, indexer_write = indexer.readresult, indexer.writeresult + + if indexer_read == indexer_write then + table_set_indexer_or_remove( + input, + indexer_index, + table_map_inner_components(callback(indexer_read :: type), table_map_values_deep, callback) + ) + else + if indexer_read then + table_set_read_indexer_or_remove( + input, + indexer_index, + table_map_inner_components(callback(indexer_read), table_map_values_deep, callback) + ) + end + + if indexer_write then + table_set_read_indexer_or_remove( + input, + indexer_index, + table_map_inner_components(callback(indexer_write), table_map_values_deep, callback) + ) + end + end + end + + return input +end + +type function table_map_values_shallow(input: typeof(types.newtable()), callback: (input: type) -> type?) + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + + if value_read == value_write then + input:setproperty(key, callback(value_read :: type)) + else + if value_read then + input:setreadproperty(key, callback(value_read)) + end + + if value_write then + input:setwriteproperty(key, callback(value_write)) + end + end + end + + local indexer = input:indexer() + if indexer then + local indexer_index = indexer.index + local indexer_read, indexer_write = indexer.readresult, indexer.writeresult + + if indexer_read == indexer_write then + table_set_indexer_or_remove(input, indexer_index, callback(indexer_read)) + else + if indexer_read then + table_set_read_indexer_or_remove(input, indexer_index, callback(indexer_read)) + end + + if indexer_write then + table_set_read_indexer_or_remove(input, indexer_index, callback(indexer_write)) + end + end + end + + return input +end + +type function table_map_inner_components( + input: type, + callback: (input: typeof(types.newtable()), ...any) -> typeof(types.newtable()), + ...: any +) + local input_tag = input.tag + return if input_tag == "union" + then union_from_components(components_map(input:components(), table_map_inner_components, callback, ...)) + or types.never + + elseif input_tag == "intersection" then intersection_from_components( + components_map(input:components(), table_map_inner_components, callback, ...) + ) or types.never + + elseif input_tag == "table" then callback(input, ...) + else input +end +---------------------------------------------------------------------------------------- + +--> Core Types ------------------------------------------------------------------------- +--[=[ + Returns a subset of a type without specified components / properties. + + @category Core + + @param input any -- The type to omit from. + @param to_omit any -- The components / properties to omit. + + @example + type Result = Omit< + "hello" | "world" | "foo" | "bar", + "world" | "bar" + > + + -- type Result = "foo" | "hello" +]=] +export type function Omit(input: type, to_omit: type) + local input_tag = innermost_tag(input) + + return if input_tag == "table" + then OmitTableByKeys(input, to_omit) + + elseif input_tag == "intersection" then if components_find_type( + input:components(), + "intersection", + "table" + ) + then OmitTableByKeys(input, to_omit) + else OmitIntersection(input, to_omit) + else if components_find_type(input:components(), "union", "table") + then OmitTableByKeys(input, to_omit) + else OmitUnion(input, to_omit) +end + +--[=[ + Returns a subset of a type with only specified components / properties. + + @category Core + + @param input any -- The type to pick from. + @param to_pick any -- The components / properties to pick. + + @example + type Result = Pick< + "hello" & "world" & "foo" & "bar", + "world" | "bar" + > + + -- type Result = "bar" & "world" +]=] +export type function Pick(input: type, to_pick: type) + local input_tag = innermost_tag(input) + + return if input_tag == "table" + then PickTableByKeys(input, to_pick) + + elseif input_tag == "intersection" then if components_find_type( + input:components(), + "intersection", + "table" + ) + then PickTableByKeys(input, to_pick) + else PickIntersection(input, to_pick) + else if components_find_type(input:components(), "union", "table") + then PickTableByKeys(input, to_pick) + else PickUnion(input, to_pick) +end + +--[=[ + Combines a nested union or intersection into one union / intersection. + Combines an intersection of tables into one consolidated table whilst preserving semantics. + + @category Core + + @param input any -- The type to flatten. + + @example + type Result = Flatten<({ hello: "world" } & ({ foo: "bar" } | { lol: "kek" }))> + + --[[ + type Result = { + foo: "bar", + hello: "world" + } | { + hello: "world", + lol: "kek" + } + ]] +]=] +export type function Flatten(input: type) + return handle_negation(input, function(input: type) + return FlattenFunction(FlattenTable(unions_and_intersections_flatten(input))) + end) +end + +--[=[ + Removes duplicate components / properties from a type. + + @category Core + + @param input any -- The type to clean. + + @example + type Result = Clean<{ + age: number | number, + [boolean]: boolean | boolean + }> + + --[[ + type Result = { + [boolean]: boolean, + age: number + } + ]] +]=] +export type function Clean(input: type) + return handle_negation(input, function(input: type, input_tag: string) + local is_union = input_tag == "union" + local is_not_union = not is_union + local is_intersection = is_not_union and input_tag == "intersection" + + if is_not_union and not is_intersection and input_tag ~= "table" and input_tag ~= "function" then + return input + end + + return CleanTable(CleanFunction(if is_union + then union_from_components( + components_clean_unions_and_intersections( + input:components(), + types.newtable(), + {}, + input_tag, + "intersection", + union_from_components, + intersection_from_components + ) + ) + + elseif is_intersection then intersection_from_components( + components_clean_unions_and_intersections( + input:components(), + types.newtable(), + {}, + input_tag, + "union", + intersection_from_components, + union_from_components + ) + ) + else input)) + end) +end +---------------------------------------------------------------------------------------- + +--> Union Types ------------------------------------------------------------------------ +--[=[ + Returns a subset of a union without specified components. + + @category Union + + @param input any -- The union to omit from. + @param to_omit any -- The components to omit. + + @example + type Result = OmitUnion< + "hello" | string | "world", + "world" + > + + -- type Result = "hello" | string +]=] +export type function OmitUnion(input: type, to_omit: type) + return handle_negation(input, function(input: type, input_tag: string) + if input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), OmitUnion, to_omit)) + end + + if to_omit.tag == "union" then + local hashset, non_hashsettable = hashset_from_components(FlattenUnion(to_omit):components()) + + return union_filter(input, input_tag, function(component): boolean + return hashset_has_type(hashset, non_hashsettable, component, component.tag) + end) or types.never + else + return union_filter(input, input_tag, function(component: type) + return component == to_omit + end) or types.never + end + end) +end + +--[=[ + Returns a subset of a union with only specified components. + + @category Union + + @param input any -- The union to pick from. + @param to_pick any -- The components to pick. + + @example + type Result = PickUnion< + "hello" | string | "world", + "world" + > + + -- type Result = "world" +]=] +export type function PickUnion(input: type, to_pick: type) + return handle_negation(input, function(input: type, input_tag: string) + if input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), PickUnion, to_pick)) + end + + if to_pick.tag == "union" then + local hashset, non_hashsettable = hashset_from_components(FlattenUnion(to_pick):components()) + + return union_filter(input, input_tag, function(component): boolean + return not hashset_has_type(hashset, non_hashsettable, component, component.tag) + end) or types.never + else + return union_filter(input, input_tag, function(component: type) + return component ~= to_pick + end) or types.never + end + end) +end + +--[=[ + Recursively combines nested unions into one union. + + @category Union + + @param input any -- The union to flatten. + + @example + type Result = FlattenUnion<"foo" | ("hello" | ("world" | "lol"))> + + -- type Result = "foo" | "hello" | "lol" | "world" +]=] +export type function FlattenUnion(input: type) + return handle_negation(input, function(input: type, input_tag: string) + if input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), FlattenUnion)) + end + + if input_tag == "union" then + return union_from_components(components_flatten(input:components(), "union", union_from_components)) + or types.never + else + return input + end + end) +end + +--[=[ + Removes duplicate types from a union. + + @category Union + + @param input any -- The union to clean. + + @example + type Result = CleanUnion<"hello" | string | "world" | string | "foo" | "hello"> + + -- type Result = "foo" | "hello" | "world" | string +]=] +export type function CleanUnion(input: type) + return handle_negation(input, function(input: type, input_tag: string) + if input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), CleanUnion)) + end + + if input_tag == "union" then + return union_from_components( + components_clean(input:components(), "union", union_from_components, types.newtable(), {}) + ) or types.never + else + return input + end + end) +end +---------------------------------------------------------------------------------------- + +--> Intersection Types ----------------------------------------------------------------- +--[=[ + Returns a subset of an intersection without specified components. + + @category Intersection + + @param input any -- The intersection to omit from. + @param to_omit any -- The components to omit. + + @example + type Result = OmitIntersection< + "hello" & string & "world", + "world" + > + + -- type Result = "hello" & string +]=] +export type function OmitIntersection(input: type, to_omit: type) + return handle_negation(input, function(input: type, input_tag: string) + if input_tag == "union" then + return union_from_components(components_map(input:components(), OmitIntersection, to_omit)) + end + + if to_omit.tag == "union" then + local hashset, non_hashsettable = hashset_from_components(FlattenUnion(to_omit):components()) + + return intersection_filter(input, input_tag, function(component): boolean + return hashset_has_type(hashset, non_hashsettable, component, component.tag) + end) or types.never + else + return intersection_filter(input, input_tag, function(component: type) + return component == to_omit + end) or types.never + end + end) +end + +--[=[ + Returns a subset of an intersection with only specified components. + + @category Intersection + + @param input any -- The intersection to pick from. + @param to_pick any -- The components to pick. + + @example + type Result = PickIntersection< + "hello" & string & "world", + "world" + > + + -- type Result = "world" +]=] +export type function PickIntersection(input: type, to_pick: type) + return handle_negation(input, function(input: type, input_tag: string) + if input_tag == "union" then + return union_from_components(components_map(input:components(), PickIntersection, to_pick)) + end + + assert_is(input, "input", input_tag, "intersection") + + if to_pick.tag == "union" then + local hashset, non_hashsettable = hashset_from_components(FlattenUnion(to_pick):components()) + + return intersection_filter(input, input_tag, function(component): boolean + return not hashset_has_type(hashset, non_hashsettable, component, component.tag) + end) or types.never + else + return intersection_filter(input, input_tag, function(component: type) + return component ~= to_pick + end) or types.never + end + end) +end + +--[=[ + Combines nested intersections into one intersection. + + @category Intersection + + @param input any -- The intersection to flatten. + + @example + type Result = FlattenIntersection<"foo" & ("hello" & ("world" & "lol"))> + + -- type Result = "foo" & "hello" & "lol" & "world" +]=] +export type function FlattenIntersection(input: type) + return handle_negation(input, function(input: type, input_tag: string) + if input_tag == "union" then + return union_from_components(components_map(input:components(), FlattenIntersection)) or types.never + end + + assert_is(input, "input", input_tag, "intersection") + + if input_tag == "intersection" then + return intersection_from_components( + components_flatten(input:components(), "intersection", intersection_from_components) + ) or types.never + else + return input + end + end) +end + +--[=[ + Removes duplicate types from an intersection. + + @category Intersection + + @param input any -- The intersection to clean. + + @example + type Result = CleanIntersection<"hello" & string & "world" & string & "foo" & "hello"> + + -- type Result = "foo" & "hello" & "world" & string +]=] +export type function CleanIntersection(input: type) + return handle_negation(input, function(input: type, input_tag: string) + if input_tag == "union" then + return union_from_components(components_map(input:components(), CleanIntersection)) or types.never + end + + assert_is(input, "input", input_tag, "intersection") + + if input_tag == "intersection" then + return intersection_from_components( + components_clean(input:components(), "intersection", intersection_from_components, types.newtable(), {}) + ) or types.never + else + return input + end + end) +end +---------------------------------------------------------------------------------------- + +--> Table Types ------------------------------------------------------------------------ +--[=[ + Returns a subset of a table without properties whose keys contain the specified components. + + @category Table + + @param input { [any]: type } -- The table to omit from. + @param to_omit any -- The values of the properties to omit. + + @example + type Result = OmitTableByKeys<{ hello: string, foo: "bar", bar: number }, "hello" | "foo"> + + -- type Result = type Result = { bar: number } +]=] +export type function OmitTableByKeys(input: typeof(types.newtable()), to_omit: type) + local input_tag = input.tag + + if input_tag == "union" then + return union_from_components(components_map(input:components(), OmitTableByKeys, to_omit)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), OmitTableByKeys, to_omit)) or types.never + end + + assert_is(input, "input", input_tag, "table") + + if to_omit.tag == "union" then + local hashset, non_hashsettable = hashset_from_components(FlattenUnion(to_omit):components()) + + return table_filter_by_keys(input, function(component: type) + return if hashset_has_type(hashset, non_hashsettable, component, component.tag) then true else false + end) + else + return table_filter_by_keys(input, function(component: type) + return component == to_omit + end) + end +end + +--[=[ + Returns a subset of a table without properties whose values contain the specified components. + + @category Table + + @param input { [any]: type } -- The table to omit from. + @param to_omit any -- The values of the properties to omit. + + @example + type Result = OmitTableByValues<{ hello: string, foo: "bar", bar: number }, "bar" | string> + + -- type Result = type Result = { bar: number } +]=] +export type function OmitTableByValues(input: typeof(types.newtable()), to_omit: type) + local input_tag = input.tag + + if input_tag == "union" then + return union_from_components(components_map(input:components(), OmitTableByValues, to_omit)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), OmitTableByValues, to_omit)) + or types.never + end + + assert_is(input, "input", input_tag, "table") + + if to_omit.tag == "union" then + local hashset, non_hashsettable = hashset_from_components(FlattenUnion(to_omit):components()) + + return table_filter_by_values(input, function(component: type) + return if hashset_has_type(hashset, non_hashsettable, component, component.tag) then true else false + end) + else + return table_filter_by_values(input, function(component: type) + return component == to_omit + end) + end +end + +--[=[ + Returns a subset of a table with only properties whose keys contain the specified components. + + @category Table + + @param input { [any]: type } -- The table to pick from. + @param to_pick any -- The keys of the properties to pick. + + @example + type Result = PickTableByKeys<{ hello: string, foo: "bar", bar: number }, "hello" | "foo"> + + -- type Result = type Result = { foo: "bar", hello: string } +]=] +export type function PickTableByKeys(input: typeof(types.newtable()), to_pick: type) + local input_tag = input.tag + + if input_tag == "union" then + return union_from_components(components_map(input:components(), PickTableByKeys, to_pick)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), PickTableByKeys, to_pick)) or types.never + end + + assert_is(input, "input", input_tag, "table") + + if to_pick.tag == "union" then + local hashset, non_hashsettable = hashset_from_components(FlattenUnion(to_pick):components()) + + return table_filter_by_keys(input, function(component: type) + return if not hashset_has_type(hashset, non_hashsettable, component, component.tag) then true else false + end) + else + return table_filter_by_keys(input, function(component: type) + return component ~= to_pick + end) + end +end + +--[=[ + Returns a subset of a table with only properties whose values contain the specified components. + + @category Table + + @param input { [any]: type } -- The table to pick from. + @param to_pick any -- The values of the properties to pick. + + @example + type Result = PickTableByValues<{ hello: string, foo: "bar", bar: number }, "bar" | string> + + -- type Result = type Result = { foo: "bar", hello: string } +]=] +export type function PickTableByValues(input: typeof(types.newtable()), to_pick: type) + local input_tag = input.tag + + if input_tag == "union" then + return union_from_components(components_map(input:components(), PickTableByValues, to_pick)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), PickTableByValues, to_pick)) + or types.never + end + + assert_is(input, "input", input_tag, "table") + + if to_pick.tag == "union" then + local hashset, non_hashsettable = hashset_from_components(FlattenUnion(to_pick):components()) + + return table_filter_by_values(input, function(component: type) + return if not hashset_has_type(hashset, non_hashsettable, component, component.tag) then true else false + end) + else + return table_filter_by_values(input, function(component: type) + return component ~= to_pick + end) + end +end + +--[=[ + Combines an intersection of tables into one consolidated table whilst preserving semantics. + + @category Table + + @param input { [any]: type } -- The table to flatten. + + @example + type Result = FlattenTable< + { name: string, salary: number } & + { kind: "employee" } + > + + --[[ + type Result = { + name: string, + salary: number, + kind: "employee" + } + ]] +]=] +export type function FlattenTable(input: type) + local input_tag = input.tag + if input_tag == "table" then + return input + end + if input_tag == "union" then + return union_from_components(components_map(input:components(), FlattenTable)) or types.never + end + if input_tag ~= "intersection" then + return input + end + + local components = components_map(input:components(), FlattenTable) + + local output: typeof(types.newtable()), output_idx: number + local can_add_read_indexer, can_add_write_indexer + + local output_union_components: { [number]: type } = {} + local output_non_table_intersection_components: { [number]: type } = {} + + for idx, component in components do + local component_tag = component.tag + + if component_tag == "union" then + for _, sub_component in Flatten(component):components() do + table.insert(output_union_components :: any, sub_component) + end + elseif component_tag == "intersection" then + local sub_components = Flatten(component):components() + local sub_output_idx + for sub_idx, sub_component in sub_components do + if sub_component.tag == "table" then + output = sub_component :: typeof(types.newtable()) + can_add_read_indexer = not output:readindexer() + can_add_write_indexer = not output:writeindexer() + + sub_output_idx = sub_idx + output_idx = idx + break + end + + table.insert(output_non_table_intersection_components, sub_component :: type) + end + + if sub_output_idx then + for sub_idx = sub_output_idx + 1, #sub_components do + local sub_component = sub_components[sub_idx] + if sub_component.tag == "table" then + can_add_read_indexer, can_add_write_indexer = + table_flatten_with(output, sub_component, can_add_read_indexer, can_add_write_indexer) + else + table.insert(output_non_table_intersection_components, sub_component :: type) + end + end + break + end + elseif component_tag == "table" then + output = component + can_add_read_indexer = not output:readindexer() + can_add_write_indexer = not output:writeindexer() + + output_idx = idx + break + else + table.insert(output_non_table_intersection_components, component) + end + end + + if not output then + return intersection_from_components(components) + end + + local components_len = #components + for idx = components_len, output_idx + 1, -1 do + local component = components[idx] + local component_tag = component.tag + + if component_tag == "union" then + for _, sub_component in Flatten(component):components() do + table.insert(output_union_components :: any, sub_component) + end + elseif component_tag == "intersection" then + for _, sub_component in Flatten(component):components() do + if sub_component.tag == "table" then + can_add_read_indexer, can_add_write_indexer = + table_flatten_with(output, sub_component, can_add_read_indexer, can_add_write_indexer) + else + table.insert(output_non_table_intersection_components, sub_component :: type) + end + end + elseif component_tag == "table" then + can_add_read_indexer, can_add_write_indexer = + table_flatten_with(output, component, can_add_read_indexer, can_add_write_indexer) + else + table.insert(output_non_table_intersection_components, component) + end + end + + if #output_union_components ~= 0 then + output = union_from_components(components_map(output_union_components, function(component) + if component.tag == "table" then + local output_copy = types.copy(output) + table_flatten_with(output_copy, component, can_add_read_indexer, can_add_write_indexer) + return output_copy + else + return types.intersectionof(output, component) + end + end)) + end + + if #output_non_table_intersection_components ~= 0 then + local output_components = output_non_table_intersection_components + table.insert(output_non_table_intersection_components, output :: type) + + output = intersection_from_components(output_components) + end + + return output +end + +--[=[ + Removes duplicate types from union and intersection keys and values inside the specified table. + + @category Table + + @param input { [any]: type } -- The table to clean. + + @example + type Result = TableClean<{ name: string | string, salary: number }> + + -- type Result = { name: string, salary: number } +]=] +export type function CleanTable(input: typeof(types.newtable())) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), CleanTable)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), CleanTable)) or types.never + end + + if input_tag ~= "table" then + return input + end + + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + + if value_read == value_write then + input:setproperty(key, Clean(value_read)) + else + if value_read then + input:setreadproperty(key, Clean(value_read)) + end + + if value_write then + input:setwriteproperty(key, Clean(value_write)) + end + end + end + + local indexer = input:indexer() + if indexer then + local indexer_read, indexer_write = indexer.readresult, indexer.writeresult + + if indexer_read and indexer_write then + local cleaned_indexer_index = Clean(indexer.index) + + if indexer_read == indexer_write then + input:setindexer(cleaned_indexer_index, Clean(indexer_read)) + else + input:setreadindexer(cleaned_indexer_index, Clean(indexer_read)) + input:setwriteindexer(cleaned_indexer_index, Clean(indexer_write)) + end + else + if indexer_read then + input:setreadindexer(Clean(indexer.index), Clean(indexer_read)) + elseif indexer_write then + input:setwriteindexer(Clean(indexer.index), Clean(indexer_write)) + end + end + end + + return input +end + +type function partial_core( + input: typeof(types.newtable()), + map_callback: ( + input: typeof(types.newtable()), + callback: (input: type) -> type? + ) -> typeof(types.newtable()) +) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), partial_core, map_callback)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), partial_core, map_callback)) + or types.never + end + + assert_is(input, "input", input_tag, "table") + + local nil_singleton = types.singleton(nil) + + return map_callback(input, function(value: type) + if input.tag == "union" then + local components = input:components() + + if + components_find(components, "union", function(component: type) + return component == nil_singleton + end) + then + return value + else + table.insert(components, nil_singleton) + return union_from_components(components) + end + else + return types.optional(value) + end + end) +end + +--[=[ + Makes every property in a table optional. + + @category Table + + @param input { [any]: type } -- The table to make partial. + + @example + type Result = Partial<{ hello: "world", foo: "bar" }> + + -- type Result = { foo: "bar"?, hello: "world"? } +]=] +export type function Partial(input: typeof(types.newtable())) + return partial_core(input, table_map_values_shallow) +end + +--[=[ + Makes every property (including nested ones) in a table optional. + + @category Table + + @param input { [any]: type } -- The table to make deeply partial. + + @example + type Result = DeepPartial<{ hello: "world", foo: { bar: "baz" } }> + + -- type Result = { foo: { bar: "baz"? }?, hello: "world"? } +]=] +export type function DeepPartial(input: typeof(types.newtable())) + return partial_core(input, table_map_values_deep) +end + +type function required_core( + input: typeof(types.newtable()), + map_callback: ( + input: typeof(types.newtable()), + callback: (input: type) -> type? + ) -> typeof(types.newtable()) +) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), required_core, map_callback)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), required_core, map_callback)) + or types.never + end + + assert_is(input, "input", input_tag, "table") + + local nil_singleton = types.singleton(nil) + + return map_callback(input, function(value) + return OmitUnion(value, nil_singleton) + end) +end + +--[=[ + Makes every property in a table required. + + @category Table + + @param input { [any]: type } -- The table to make required. + + @example + type Result = Required<{ hello: "world"?, foo: "bar"? }> + + -- type Result = { foo: "bar", hello: "world" } +]=] +export type function Required(input: typeof(types.newtable())) + return required_core(input, table_map_values_shallow) +end + +--[=[ + Makes every property (including nested ones) in a table required. + + @category Table + + @param input { [any]: type } -- The table to make deeply required. + + @example + type Result = DeepRequired<{ hello: { lol: "kek"? }?, foo: "bar"? }> + + -- type Result = { foo: { lol: "kek" }, hello: "world" } +]=] +export type function DeepRequired(input: typeof(types.newtable())) + return required_core(input, table_map_values_deep) +end + +--[=[ + Makes every property in a table read only. + + @category Table + + @param input { [any]: type } -- The table to make read only. + + @example + type Result = ReadOnly<{ hello: "world", foo: "bar" }> + + -- type Result = { read foo: "bar", read hello: "world" } +]=] +export type function ReadOnly(input: typeof(types.newtable())) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), ReadOnly)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), ReadOnly)) or types.never + end + + assert_is(input, "input", input_tag, "table") + + for key, value in input:properties() do + if value.write then + input:setwriteproperty(key, nil) + end + end + + -- TODO: add support for read and write indexers when available. + + return input +end + +type function deep_read_only_core(input: typeof(types.newtable())) + for key, value in input:properties() do + if value.write then + input:setwriteproperty(key, nil) + end + input:setreadproperty(key, table_map_inner_components(value.read, deep_read_only_core)) + end + + -- TODO: add support for read and write indexers when available. + + return input +end + +--[=[ + Makes every property (including nested ones) in a table read only. + + @category Table + + @param input { [any]: type } -- The table to make deeply read only. + + @example + type Result = DeepReadOnly<{ hello: "world", foo: { bar: "baz" } }> + + -- type Result = { read foo: { read bar: "baz" }, read hello: "world" } +]=] +export type function DeepReadOnly(input: typeof(types.newtable())) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), DeepReadOnly)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), DeepReadOnly)) or types.never + end + + assert_is(input, "input", input_tag, "table") + + return deep_read_only_core(input) +end + +--[=[ + Makes every property in a table readable and writable. + + @category Table + + @param input { [any]: type } -- The table to make readable and writable. + + @example + type Result = ReadWrite<{ read hello: "world", foo: "bar" }> + + -- type Result = { foo: "bar", hello: "world" } +]=] +export type function ReadWrite(input: typeof(types.newtable())) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), ReadWrite)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), ReadWrite)) or types.never + end + + assert_is(input, "input", input_tag, "table") + + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + + if value_read and not value_write then + input:setproperty(key, value_read) + elseif value_write and not value_read then + input:setproperty(key, value_write) + end + end + + -- TODO: add support for read and write indexers when available. + + return input +end + +type function deep_read_write_core(input: typeof(types.newtable())) + for key, value in input:properties() do + local value_read, value_write = value.read, value.write + + if value_read and not value_write then + input:setproperty(key, table_map_inner_components(value_read, deep_read_write_core)) + elseif value_write and not value_read then + input:setproperty(key, table_map_inner_components(value_write, deep_read_write_core)) + end + end + + -- TODO: add support for read and write indexers when available. + + return input +end + +--[=[ + Makes every property (including nested ones) in a table readable and writable. + + @category Table + + @param input { [any]: type } -- The table to make deeply readable and writable. + + @example + type Result = DeepReadWrite<{ read hello: "world", foo: { read bar: "baz" } }> + + -- type Result = { foo: { bar: "baz" }, hello: "world" } +]=] +export type function DeepReadWrite(input: typeof(types.newtable())) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), DeepReadWrite)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), DeepReadWrite)) or types.never + end + + assert_is(input, "input", input_tag, "table") + + return deep_read_write_core(input) +end +---------------------------------------------------------------------------------------- + +--> Function Types --------------------------------------------------------------------- +--[=[ + Flattens unions, intersections and tables inside function parameter and return types. + + @category Function + + @param input (...any) -> (...any) -- The function to flatten. + + @example + type Result = FlattenFunction<(string | (number | boolean)) -> (any & (boolean & number))> + + -- type Result = (boolean | number | string) -> any & boolean & number +]=] +export type function FlattenFunction(input: typeof(types.newfunction())) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), FlattenFunction)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), FlattenFunction)) or types.never + end + + if input_tag ~= "function" then + return input + end + + local input_params = input:parameters() + local input_params_head, input_params_tail = input_params.head, input_params.tail + + if input_params_head then + for idx, param in input_params_head do + input_params_head[idx] = Flatten(param) + end + end + + if input_params_tail then + input_params_tail = Flatten(input_params_tail) + end + + local input_returns = input:returns() + local input_returns_head, input_returns_tail = input_returns.head, input_returns.tail + + if input_returns_head then + for idx, returns_item in input_returns_head do + input_returns_head[idx] = Flatten(returns_item) + end + end + + if input_returns_tail then + input_returns_tail = Flatten(input_returns_tail) + end + + return types.newfunction( + { head = input_params_head, tail = input_params_tail }, + { head = input_returns_head, tail = input_returns_tail }, + input:generics() + ) +end + +--[=[ + Removes duplicate components / properties from function param and return types. + + @category Function + + @param input (...any) -> (...any) -- The function to clean. + + @example + type Result = CleanFunction<(number, string | string | boolean) -> any | any> + + -- type Result = (number, boolean | string) -> any +]=] +export type function CleanFunction(input: typeof(types.newfunction())) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), CleanFunction)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), CleanFunction)) or types.never + end + + if input_tag ~= "function" then + return input + end + + local input_params = input:parameters() + local input_params_head, input_params_tail = input_params.head, input_params.tail + + if input_params_head then + for idx, param in input_params_head do + input_params_head[idx] = Clean(param) + end + end + + if input_params_tail then + input_params_tail = Clean(input_params_tail) + end + + local input_returns = input:returns() + local input_returns_head, input_returns_tail = input_returns.head, input_returns.tail + + if input_returns_head then + for idx, returns_item in input_returns_head do + input_returns_head[idx] = Clean(returns_item) + end + end + + if input_returns_tail then + input_returns_tail = Clean(input_returns_tail) + end + + return types.newfunction( + { head = input_params_head, tail = input_params_tail }, + { head = input_returns_head, tail = input_returns_tail }, + input:generics() + ) +end + +--[=[ + Gets the parameters for a function. + + @category Function + + @param input (...any) -> (...any) -- The function to get params for. + + @example + type Result = Params<(number, string, boolean, ...{ any }) -> any> + + --[[ + type Result = { + 1: number, + 2: string, + 3: boolean, + tail: {any} + } + ]] +]=] +export type function Params(input: typeof(types.newfunction())) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), Params)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), Params)) or types.never + end + + assert_is(input, "input", input_tag, "function") + + local input_params = input:parameters() + local input_params_head, input_params_tail = input_params.head, input_params.tail + + local output = types.newtable() + + if input_params_head then + for idx, param in input_params_head do + output:setproperty(types.singleton(`{idx}`), param) + end + end + + if input_params_tail then + output:setproperty(types.singleton("tail"), input_params_tail) + end + + return output +end + +--[=[ + Gets the return types for a function. + + @category Function + + @param input (...any) -> (...any) -- The function to get return types for. + + @example + type Result = Returns<() -> (string, number)> + + -- type Result = { 1: string, 2: number } +]=] +export type function Returns(input: typeof(types.newfunction())) + local input_tag = input.tag + if input_tag == "union" then + return union_from_components(components_map(input:components(), Returns)) or types.never + elseif input_tag == "intersection" then + return intersection_from_components(components_map(input:components(), Returns)) or types.never + end + + assert_is(input, "input", input_tag, "function") + + local input_returns = input:returns() + local input_returns_head, input_returns_tail = input_returns.head, input_returns.tail + + local output = types.newtable() + + if input_returns_head then + for idx, param in input_returns_head do + output:setproperty(types.singleton(`{idx}`), param) + end + end + + if input_returns_tail then + output:setproperty(types.singleton(`tail`), input_returns_tail) + end + + return output +end +---------------------------------------------------------------------------------------- + +--> Miscellaneous Types ---------------------------------------------------------------- +--[=[ + Returns the inputted type but with unions, intersections and negations turned into tables so they can be inspected / debugged better. + + @category Miscellaneous + + @param input any -- The type to be inspected. + + @example + type Result = Inspect<"hello" | ("world" & string)> + + --[[ + type Result = { + 1: "hello", + 2: { + 1: "world", + 2: string, + kind: "intersection" + }, + kind: "union" + } + ]] +]=] +export type function Inspect(input: type) + local input_tag = input.tag + + if input_tag == "negation" then + return types.newtable({ + [types.singleton("1")] = Inspect(input:inner()), + [types.singleton("kind")] = types.singleton("negation"), + }) + elseif input_tag == "table" or input_tag == "singleton" or is_primitive(input_tag) then + return input + else + local output = types.newtable({ + [types.singleton("kind")] = types.singleton(input_tag), + }) + + for idx, component in input:components() do + local component_tag = component.tag + + output:setproperty( + types.singleton(`{idx}`), + if component_tag == "union" or component_tag == "intersection" then Inspect(component) else component + ) + end + + return output + end +end + +--[=[ + Throws a type error if the first type does not equal the second. + + @category Miscellaneous + + @param expect any -- The type to be compared. + @param toBe any -- The type you want to compare `expect` to. + + @example + type test = Expect< + { foo: "bar" } & { hello: "world" }, + (string, boolean) -> ...any + > + + --[[ + TypeError: 'Expect' type function errored at runtime: [string "Expect"]:2295: + Expected the function type below: + > (string, boolean) -> (...any) + + But Got this intersection type instead: + > { + > foo: "bar" + > } & { + > hello: "world" + > } + ]] +]=] +export type function Expect(expect: type, to_be: type) + if sort_type(expect) ~= sort_type(to_be) then + local expect_tag, to_be_tag = expect.tag, to_be.tag + + local expect_name = if expect_tag == "singleton" then type(expect:value()) else expect_tag + local to_be_name = if to_be_tag == "singleton" then type(to_be:value()) else to_be_tag + + local error_msg_start = `\nExpected the {to_be_name} type below:{stringify_preview(to_be)}\n\n` + local error_msg_end = `But Got this {expect_name} type instead:{stringify_preview(expect)}` + + error(`{error_msg_start}{error_msg_end}`) + end + + return expect +end + +--[=[ + Negates a type. + + ::: warning TEMPORARY + This type function will be removed once luau gets negation syntax (see the `negation-types` RFC). + ::: + + @category Miscellaneous + + @param input any -- The type to stringify. + + @example + type Result = Negate + + -- type Result = ~string +]=] +export type function Negate(input: type) + return types.negationof(input) +end +---------------------------------------------------------------------------------------- + +return nil + +--[[ + MIT License + + Copyright (c) 2025 Cameron P Campbell + + Permission is hereby granted, free of chparame, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + contributors OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +]] diff --git a/selene-std.yaml b/selene-std.yaml index 3aacea2..e793e52 100644 --- a/selene-std.yaml +++ b/selene-std.yaml @@ -1,5 +1,7 @@ --- base: roblox globals: + types: + any: true require: any: true diff --git a/selene.toml b/selene.toml index 6a5b600..978c0c2 100644 --- a/selene.toml +++ b/selene.toml @@ -3,3 +3,4 @@ std = "selene-std" [lints] mixed_table = "allow" shadowing = "allow" +undefined_variable = "allow" diff --git a/src/utils/ids.luau b/src/utils/ids.luau index 9e40d96..f6d982a 100644 --- a/src/utils/ids.luau +++ b/src/utils/ids.luau @@ -34,6 +34,7 @@ end local function getUserThumbnail(userId: number, type: Enum.ThumbnailType, size: Enum.ThumbnailSize): string return `rbxthumb://type={THUMBNAIL_NAMES[type]}&id={userId}&w={THUMBNAIL_NAMES[size]}&h={THUMBNAIL_NAMES[size]}` end + return table.freeze({ rocket = rocket, getUsername = getUsername, From cc853ba51baa11392b972a6b0335eef872eb6fcc Mon Sep 17 00:00:00 2001 From: znotfireman Date: Sat, 24 Jan 2026 15:37:54 +0700 Subject: [PATCH 2/2] feat: Chapter 3, I Saw An Angel Heavens laid ruin, so silence learned her name. --- .gitignore | 3 + .luaurc | 3 +- .lute/.luaurc | 7 +- .lute/analyze.luau | 33 - .lute/batteries/toml.luau | 10 +- .lute/build-pesde.luau | 167 +++ .lute/build-release.luau | 15 - .lute/build.luau | 141 --- .lute/clean-build.luau | 16 - .lute/clean-pkgs.luau | 21 - .lute/clean.luau | 6 - .lute/doctor.luau | 5 + .lute/export-to-downloads.luau | 24 - .lute/lib/config.luau | 39 - .lute/lib/monorepo/compareModules.luau | 7 + .lute/lib/monorepo/configure.luau | 92 ++ .lute/lib/monorepo/types.luau | 18 + .lute/lib/monorepo/useForwardedModules.luau | 33 + .lute/lib/monorepo/useModules.luau | 60 + .lute/lib/monorepo/useRepository.luau | 25 + .lute/lib/{ => utils}/Summon.luau | 7 +- .lute/lib/utils/copyInto.luau | 25 + .lute/lib/utils/requireCwd.luau | 7 + .lute/setup-lute-typedefs.luau | 25 + .lute/setup.luau | 4 + .lute/typedefs/.luaurc | 3 + .lute/validate-configs.luau | 22 + .lute/watch-docs.luau | 5 - .lute/watch.luau | 96 -- .zed/settings.json | 45 +- __config__.luau | 36 + modules/api/__config__.luau | 10 + modules/api/init.luau | 17 +- modules/boba/__config__.luau | 6 + .../boba-base.luau => modules/boba/base.luau | 93 +- modules/boba/init.luau | 1035 +++++++++++++++++ modules/plugin/.luaurc | 5 - modules/std/__config__.luau | 11 + modules/types/__config__.luau | 6 + modules/{api-types => types}/arguments.luau | 0 modules/{api-types => types}/core.luau | 0 modules/{api-types => types}/gizmos.luau | 0 modules/{api-types => types}/init.luau | 7 +- modules/{api-types => types}/iris/events.luau | 0 modules/{api-types => types}/iris/init.luau | 0 modules/{api-types => types}/iris/types.luau | 0 .../iris/widgets/basic.luau | 0 .../iris/widgets/format.luau | 0 .../iris/widgets/image.luau | 0 .../iris/widgets/menu.luau | 0 .../iris/widgets/tab.luau | 0 .../iris/widgets/text.luau | 0 .../iris/widgets/tree.luau | 0 .../iris/widgets/window.luau | 0 modules/{api-types => types}/log.luau | 0 modules/{api-types => types}/schemas.luau | 0 modules/{api-types => types}/utils.luau | 0 .../{api-types => types}/vendor/trove.luau | 0 pesde.toml | 8 +- rocket.config.toml | 16 - {include => src/include}/.luaurc | 0 {include => src/include}/assets.luau | 0 {include => src/include}/boba.luau | 0 {include => src/include}/fzy.luau | 0 {include => src/include}/repr.luau | 0 {include => src/include}/typeforge.luau | 0 {include => src/include}/types.luau | 0 67 files changed, 1745 insertions(+), 469 deletions(-) delete mode 100644 .lute/analyze.luau create mode 100644 .lute/build-pesde.luau delete mode 100644 .lute/build-release.luau delete mode 100644 .lute/build.luau delete mode 100644 .lute/clean-build.luau delete mode 100644 .lute/clean-pkgs.luau delete mode 100644 .lute/clean.luau create mode 100644 .lute/doctor.luau delete mode 100644 .lute/export-to-downloads.luau delete mode 100644 .lute/lib/config.luau create mode 100644 .lute/lib/monorepo/compareModules.luau create mode 100644 .lute/lib/monorepo/configure.luau create mode 100644 .lute/lib/monorepo/types.luau create mode 100644 .lute/lib/monorepo/useForwardedModules.luau create mode 100644 .lute/lib/monorepo/useModules.luau create mode 100644 .lute/lib/monorepo/useRepository.luau rename .lute/lib/{ => utils}/Summon.luau (94%) create mode 100644 .lute/lib/utils/copyInto.luau create mode 100644 .lute/lib/utils/requireCwd.luau create mode 100644 .lute/setup-lute-typedefs.luau create mode 100644 .lute/setup.luau create mode 100644 .lute/typedefs/.luaurc create mode 100644 .lute/validate-configs.luau delete mode 100644 .lute/watch-docs.luau delete mode 100644 .lute/watch.luau create mode 100644 __config__.luau create mode 100644 modules/api/__config__.luau create mode 100644 modules/boba/__config__.luau rename include/boba-base.luau => modules/boba/base.luau (71%) create mode 100644 modules/boba/init.luau delete mode 100644 modules/plugin/.luaurc create mode 100644 modules/std/__config__.luau create mode 100644 modules/types/__config__.luau rename modules/{api-types => types}/arguments.luau (100%) rename modules/{api-types => types}/core.luau (100%) rename modules/{api-types => types}/gizmos.luau (100%) rename modules/{api-types => types}/init.luau (95%) rename modules/{api-types => types}/iris/events.luau (100%) rename modules/{api-types => types}/iris/init.luau (100%) rename modules/{api-types => types}/iris/types.luau (100%) rename modules/{api-types => types}/iris/widgets/basic.luau (100%) rename modules/{api-types => types}/iris/widgets/format.luau (100%) rename modules/{api-types => types}/iris/widgets/image.luau (100%) rename modules/{api-types => types}/iris/widgets/menu.luau (100%) rename modules/{api-types => types}/iris/widgets/tab.luau (100%) rename modules/{api-types => types}/iris/widgets/text.luau (100%) rename modules/{api-types => types}/iris/widgets/tree.luau (100%) rename modules/{api-types => types}/iris/widgets/window.luau (100%) rename modules/{api-types => types}/log.luau (100%) rename modules/{api-types => types}/schemas.luau (100%) rename modules/{api-types => types}/utils.luau (100%) rename modules/{api-types => types}/vendor/trove.luau (100%) delete mode 100644 rocket.config.toml rename {include => src/include}/.luaurc (100%) rename {include => src/include}/assets.luau (100%) rename {include => src/include}/boba.luau (100%) rename {include => src/include}/fzy.luau (100%) rename {include => src/include}/repr.luau (100%) rename {include => src/include}/typeforge.luau (100%) rename {include => src/include}/types.luau (100%) diff --git a/.gitignore b/.gitignore index a147594..a0d48bb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ include/assets.luau Rocket.rbxm roblox.d.luau src/_OLD_ + +.lute/typedefs/* +!.lute/typedefs/.luaurc diff --git a/.luaurc b/.luaurc index 416a461..30df68d 100644 --- a/.luaurc +++ b/.luaurc @@ -4,7 +4,8 @@ "src": "src", "include": "include", "pkgs": "roblox_packages", - "modules": "modules" + "modules": "modules", + "monorepo": ".lute/lib/monorepo" }, "lint": { "*": true diff --git a/.lute/.luaurc b/.lute/.luaurc index 36cd88b..38c0307 100644 --- a/.lute/.luaurc +++ b/.lute/.luaurc @@ -2,10 +2,9 @@ "aliases": { "batteries": "./batteries", "lib": "./lib", - "include": "../include", - "std": "~/.lute/typedefs/0.1.0/std", - "lint": "~/.lute/typedefs/0.1.0/lint", - "lute": "~/.lute/typedefs/0.1.0/lute" + "std": "./typedefs/std", + "lint": "./typedefs/lint", + "lute": "./typedefs/lute" } } diff --git a/.lute/analyze.luau b/.lute/analyze.luau deleted file mode 100644 index d971cea..0000000 --- a/.lute/analyze.luau +++ /dev/null @@ -1,33 +0,0 @@ -local Summon = require("./lib/Summon") -local config = require("@lib/config")() -local fs = require("@std/fs") -local net = require("@lute/net") -local process = require("@lute/process") - -local ROBLOX_TYPEDEFS_FILENAME = "roblox.d.luau" - -do - fs.writestringtofile( - ROBLOX_TYPEDEFS_FILENAME, - net.request("https://luau-lsp.pages.dev/globalTypes.PluginSecurity.d.luau", { method = "GET" }).body - ) - - Summon.new("rojo", "sourcemap", "--output", config.sourcemap.filename, config.sourcemap.project):assert() - process.exit(Summon.codeFromMany( - Summon.new( - "luau-lsp", - "analyze", - `--sourcemap={config.sourcemap.filename}`, - '--ignore="**/include/**"', - '--ignore="**/roblox_packages/**"', - -- FIXME: luau-lsp is analyzing other WTH projects for some reason?? - "--ignore=../../../../../..", - "--base-luaurc=.luaurc", - `--definitions=roblox.d.luau`, - "--flag:LuauSolverV2=true", - "src" - ), - Summon.new("selene", "src"), - Summon.new("stylua", "--check", "src") - )) -end diff --git a/.lute/batteries/toml.luau b/.lute/batteries/toml.luau index 4971c76..dc9707d 100644 --- a/.lute/batteries/toml.luau +++ b/.lute/batteries/toml.luau @@ -40,10 +40,14 @@ local function tableToToml(tbl: {}, parent: string) for k, v in tbl do if typeof(v) == "table" and next(v) ~= nil then if #v > 0 then - for _, entry in v do - result ..= "\n[[" .. (parent and parent .. "." or "") .. k .. "]]\n" - result ..= tableToToml(entry, nil) + result ..= k .. "=" .. "[" + for i, entry in v do + result ..= serializeValue(entry) + if i < #v then + result ..= ", " + end end + result ..= "]\n" else subTables[k] = v end diff --git a/.lute/build-pesde.luau b/.lute/build-pesde.luau new file mode 100644 index 0000000..96fa917 --- /dev/null +++ b/.lute/build-pesde.luau @@ -0,0 +1,167 @@ +local cli = require("@batteries/cli") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@lute/process") +local tableext = require("@std/tableext") +local types = require("@monorepo/types") +local useForwardedModules = require("@monorepo/useForwardedModules") +local useModules = require("@monorepo/useModules") +local useRepository = require("@monorepo/useRepository") + +local BASE_INCLUDE = { "pesde.toml", "src/*", "LICENSE.md", "README.md" } +local BASE_BUILD_FILES = { "src" } +local PESDE_MANIFEST_FOOTER = [[ +[indices] +default = "https://github.com/pesde-pkg/index" + +[scripts] +roblox_sync_config_generator = ".pesde/scripts/roblox_sync_config_generator.luau" +sourcemap_generator = ".pesde/scripts/sourcemap_generator.luau" + +[dev_dependencies] +scripts = { name = "pesde/scripts_rojo", version = "^0.2.0", target = "lune" } +rojo = { name = "pesde/rojo", version = "^7.5.1", target = "lune" } + +[engines] +pesde = "^0.7.2" +lune = "^0.10.2" +]] + +local args = cli.parser() +args:add("script", "positional", { help = "This script-" }) +args:add("help", "flag", { help = "Shows this message" }) +args:add("repoconfig", "option", { help = "Path from the CWD to repository config" }) +args:parse({ ... }) + +if args:has("help") then + print("Builds pesde packages from the repository's modules") + print() + print("To build all modules:") + print("\tlute build-pesde") + print() + print("To build a specific module, specify it in forwarded args:") + print("\tlute build-pesde boba") + print() + args:help() + return +end + +local repository = useRepository(args:get("repoconfig")) +local registries = repository.modules.registries +if not registries then + print("Registeries unspecified in repository config, aborting") + return +end +local pesdeRegistry = registries.pesde +if not pesdeRegistry then + print("pesde registery unspecified in repository config") + return +end + +local allModules = useModules(repository) +local buildModules = useForwardedModules(allModules, args:forwarded()) +local buildDir = path.join(repository.build.output, "pesde") +local modulesBuilt: { [types.Module]: true } = {} + +local function quote(str: string) + return string.format("%q", str) +end + +local function getModuleVersion(module: types.Module) + return module.config.version or repository.metadata.version +end + +local function getModuleFullname(moduleName: string) + local formattedName = string.format(pesdeRegistry.moduleFormat :: any, moduleName) + return (string.gsub(`{pesdeRegistry.scope}/{formattedName}`, "%-", "_")) +end + +local function buildModule(module: types.Module) + if modulesBuilt[module] then + return + end + + modulesBuilt[module] = true + + local builtRoot = path.join(buildDir, module.name) + local builtSrc = path.join(builtRoot, "src") + local builtManifest = path.join(builtRoot, "pesde.toml") + local builtPesde = path.join(builtRoot, ".pesde") + local builtPesdeScripts = path.join(builtPesde, "scripts") + + local manifest = {} + table.insert(manifest, `# {repository.metadata.copyright}`) + table.insert(manifest, "# This file was @autogenerated and not intended for manual editing.") + table.insert(manifest, "") + table.insert(manifest, `name = {quote(getModuleFullname(module.config.name))}`) + table.insert(manifest, `version = {quote(module.config.version or repository.metadata.version)}`) + table.insert(manifest, `description = {quote(module.config.description)}`) + table.insert(manifest, `license = {quote(module.config.license or repository.metadata.license)}`) + local authors = {} + for i, author in module.config.authors or repository.metadata.authors do + authors[i] = quote(author) + end + table.insert(manifest, `authors = [{table.concat(authors, ", ")}]`) + table.insert(manifest, `repository = {quote(repository.metadata.repository)}`) + + local includeFiles, buildFiles = table.clone(BASE_INCLUDE), table.clone(BASE_BUILD_FILES) + + if module.config.dependencies then + table.insert(manifest, "") + table.insert(manifest, "[dependencies]") + + for dependency in module.config.dependencies do + local dependentModule = allModules.nameToModule[dependency] + if not dependentModule then + print(`Got invalid dependency "{dependentModule}" in module "{module.name}"`) + process.exit(1) + return + end + + buildModule(dependentModule) + table.insert( + manifest, + dependency + .. " = { workspace = " + .. quote(getModuleFullname(dependency)) + .. ", version = " + .. quote(getModuleVersion(module)) + .. " }" + ) + + local linkerFilename = `{dependency}.luau` + table.insert(includeFiles, linkerFilename) + table.insert(buildFiles, linkerFilename) + end + end + + table.insert(manifest, 4, `include = [{table.concat(tableext.map(includeFiles, quote), ", ")}]`) + table.insert(manifest, "") + table.insert(manifest, "[target]") + table.insert(manifest, 'environment = "roblox"') + table.insert(manifest, 'lib = "src/init.luau"') + table.insert(manifest, `build_files = [{table.concat(tableext.map(buildFiles, quote), ", ")}]`) + + table.insert(manifest, "") + table.insert(manifest, PESDE_MANIFEST_FOOTER) + + fs.createdirectory(builtRoot) + fs.createdirectory(builtSrc) + fs.createdirectory(builtPesde) + fs.createdirectory(builtPesdeScripts) + + fs.writestringtofile(builtManifest, table.concat(manifest, "\n")) + + for _, entry in fs.listdirectory(".pesde/scripts") do + if entry.type == "file" then + fs.copy(path.join(".pesde", "scripts", entry.name), path.join(builtRoot, ".pesde", "scripts", entry.name)) + end + end +end + +pcall(fs.removedirectory, buildDir, { recursive = true }) +fs.createdirectory(buildDir, { recursive = true }) + +for _, module in buildModules do + buildModule(module) +end diff --git a/.lute/build-release.luau b/.lute/build-release.luau deleted file mode 100644 index a87a342..0000000 --- a/.lute/build-release.luau +++ /dev/null @@ -1,15 +0,0 @@ -local Summon = require("@lib/Summon") - -do - local build = Summon.new( - "lute", - "run", - "build", - "model", - "--mark-attribution", - "--mark-language-mode strict", - "--mark-optimization-level 2" - ) - - build:assert() -end diff --git a/.lute/build.luau b/.lute/build.luau deleted file mode 100644 index 1ba41c1..0000000 --- a/.lute/build.luau +++ /dev/null @@ -1,141 +0,0 @@ -local Summon = require("@lib/Summon") -local cli = require("@batteries/cli") -local config = require("@lib/config")() -local fs = require("@std/fs") -local path = require("@std/path") -local process = require("@lute/process") -local tableext = require("@std/tableext") - -local args = cli.parser() -args:add("output", "positional", { help = "Output as a 'plugin' or 'model'", required = true }) -args:add("mark-language-mode", "option", { help = "Marks 'strict', 'nonstrict', or 'nocheck' language mode" }) -args:add("mark-optimization-level", "option", { help = "Marks '0', '1', or '2' optimization level" }) -args:add("mark-attribution", "flag", { help = "Include Rocket's attribution comment" }) - -local cwd = process.cwd() - -local function createHeaderComment() - local headerComment = {} - - local languageMode = args:get("mark-language-mode") - if languageMode then - table.insert(headerComment, `--!{languageMode}`) - end - - local optimizationLevel = args:get("mark-optimization-level") - if optimizationLevel then - table.insert(headerComment, `--!optimize {optimizationLevel}`) - end - - if #headerComment > 0 then - table.insert(headerComment, "") - end - - if args:has("mark-attribution") then - table.insert(headerComment, `-- {config.copyright}:`) - table.insert(headerComment, `-- https://github.com/team-fireworks/rocket`) - table.insert(headerComment, `--`) - table.insert(headerComment, `-- This Source Code Form is subject to the terms of the Mozilla Public License,`) - table.insert(headerComment, `-- v. 2.0. If a copy of the MPL was not distributed with this file, You can`) - table.insert(headerComment, `-- obtain one at http://mozilla.org/MPL/2.0/.`) - table.insert(headerComment, "\n") - end - - return headerComment -end - -local function copy(from: string, to: string) - local walkInput = fs.walk(from, { recursive = true }) - local entry = walkInput() - local copied = {} - - while entry do - local relative = string.sub(path.format(entry), #from + 2) - local destination = path.join(to, relative) - if fs.type(entry) == "file" then - fs.createdirectory(path.dirname(destination), { makeparents = true }) - fs.copy(entry, destination) - copied[destination] = true - end - entry = walkInput() - end - - return copied -end - -local function prepareTooling() - local output = args:get("output") - local modelPath = path.join(cwd, config.build.model) - - if output == "model" and fs.exists(modelPath) then - fs.remove(modelPath) - end - - local sourcemapPath = path.join(cwd, config.sourcemap.filename) - if fs.exists(sourcemapPath) then - fs.remove(sourcemapPath) - end -end - -local function prepareOutput() - local output = path.join(cwd, config.build.output) - if fs.exists(path.join(cwd, config.build.output)) then - fs.removedirectory(output, { recursive = true }) - end - - return tableext.combine( - copy(path.format(path.join(cwd, config.build.src)), path.format(output)), - copy( - path.format(path.join(cwd, config.build.extensions)), - path.format(path.join(cwd, config.build.output, config.build.extensions)) - ) - ) -end - -local function prepareBuild() - prepareTooling() - return prepareOutput() -end - -local function parseArgs(...: any) - local commandArgs = { ... } - table.remove(commandArgs, 1) - if #commandArgs == 0 or commandArgs[1] == "help" then - args:help() - return - end - - args:parse(commandArgs) -end - -parseArgs(...) -do - local buildFiles = prepareBuild() - - Summon.new("rojo", "sourcemap", "--output", config.sourcemap.filename, config.sourcemap.project):assert() - Summon.new("darklua", "process", config.build.src, config.build.output):assert() - Summon.new( - "darklua", - "process", - config.build.extensions, - path.format(path.join(config.build.output, config.build.extensions)) - ):assert() - - local headerComment = createHeaderComment() - if next(headerComment) ~= nil then - local formattedHeaderComment = table.concat(headerComment, "\n") - for out in buildFiles do - fs.writestringtofile(out, formattedHeaderComment .. fs.readfiletostring(out)) - end - end - - Summon.new( - "rojo", - "build", - if args:get("output") == "plugin" then "--plugin" else "--output", - config.build.model, - config.build.project - ):assert() - - process.exit(0) -end diff --git a/.lute/clean-build.luau b/.lute/clean-build.luau deleted file mode 100644 index 31e242e..0000000 --- a/.lute/clean-build.luau +++ /dev/null @@ -1,16 +0,0 @@ -local config = require("@lib/config")() -local fs = require("@std/fs") -local path = require("@std/path") -local process = require("@lute/process") - -local function removeSafe(remover: (string, Args...) -> (), filepath: string, ...: Args...) - if fs.exists(filepath) then - remover(filepath, ...) - end -end - -do - local cwd = process.cwd() - removeSafe(fs.removedirectory, path.join(cwd, config.build.output), { recursive = true }) - removeSafe(fs.remove, path.join(cwd, config.sourcemap.filename)) -end diff --git a/.lute/clean-pkgs.luau b/.lute/clean-pkgs.luau deleted file mode 100644 index 1dd38c5..0000000 --- a/.lute/clean-pkgs.luau +++ /dev/null @@ -1,21 +0,0 @@ -local fs = require("@std/fs") -local path = require("@std/path") -local process = require("@lute/process") - -local PESDE_ARTIFACTS: { [string]: (string) -> () } = { - luau_packages = fs.removedirectory, - lune_packages = fs.removedirectory, - roblox_packages = fs.removedirectory, - - ["pesde.lock"] = fs.remove, -} - -do - local cwd = process.cwd() - for artifact, remover in PESDE_ARTIFACTS do - local artifactPath = path.join(cwd, artifact) - if fs.exists(artifactPath) then - remover(artifactPath, { recursive = true }) - end - end -end diff --git a/.lute/clean.luau b/.lute/clean.luau deleted file mode 100644 index 07fcf75..0000000 --- a/.lute/clean.luau +++ /dev/null @@ -1,6 +0,0 @@ -local Summon = require("@lib/Summon") -local process = require("@lute/process") - -do - process.exit(Summon.codeFromMany(Summon.new("lute", "clean-build"), Summon.new("lute", "clean-pkgs"))) -end diff --git a/.lute/doctor.luau b/.lute/doctor.luau new file mode 100644 index 0000000..1033818 --- /dev/null +++ b/.lute/doctor.luau @@ -0,0 +1,5 @@ +local Summon = require("@lib/utils/Summon") +local process = require("@lute/process") + +print("Doctoring Rocket...") +process.exit(Summon.codeFromMany(Summon.new("lute", "validate-configs"))) diff --git a/.lute/export-to-downloads.luau b/.lute/export-to-downloads.luau deleted file mode 100644 index 6341764..0000000 --- a/.lute/export-to-downloads.luau +++ /dev/null @@ -1,24 +0,0 @@ -local Summon = require("@lib/Summon") -local fs = require("@std/fs") -local path = require("@std/path") -local process = require("@lute/process") -local system = require("@lute/system") - -do - local now = os.time() - Summon.new("lute", "run", "build-release"):assert() - - if system.os == "Darwin" then - local model = path.join(process.cwd(), "Rocket.rbxm") - local timestamp = os.date("(%B %d, %Y at %I:%M:%S %p)", now) - local filename = `Rocket Pre-Alpha {timestamp}.rbxm` - local destination = path.join(process.homedir(), "Downloads", filename) - - fs.copy(model, destination) - print("Exported to " .. path.format(destination)) - process.exit(0) - else - print("Unsupported operating system: " .. system.os) - process.exit(1) - end -end diff --git a/.lute/lib/config.luau b/.lute/lib/config.luau deleted file mode 100644 index 60bcf9a..0000000 --- a/.lute/lib/config.luau +++ /dev/null @@ -1,39 +0,0 @@ -local Boba = require("@include/boba") -local fs = require("@std/fs") -local path = require("@std/path") -local process = require("@lute/process") -local toml = require("@batteries/toml") - -local Config = Boba.Struct { - version = Boba.String.inner, - copyright = Boba.String.inner, - - flags = Boba.Map(Boba.String, Boba.Unknown).inner, - - sourcemap = Boba.Struct { - filename = Boba.String.inner, - project = Boba.String.inner, - }.inner, - - build = Boba.Struct { - src = Boba.String.inner, - extensions = Boba.String.inner, - - output = Boba.String.inner, - model = Boba.String.inner, - project = Boba.String.inner, - }.inner, -}:Nickname("Config") - -export type Config = typeof(Config.inner) - -local function config() - local CONFIG_PATH = path.join(process.cwd(), "rocket.config.toml") - if fs.exists(CONFIG_PATH) then - return Config:assert(toml.deserialize(fs.readfiletostring(CONFIG_PATH))) - end - print("Missing `rocket.config.toml` configuration!") - return process.exit(1) -end - -return config diff --git a/.lute/lib/monorepo/compareModules.luau b/.lute/lib/monorepo/compareModules.luau new file mode 100644 index 0000000..1c80609 --- /dev/null +++ b/.lute/lib/monorepo/compareModules.luau @@ -0,0 +1,7 @@ +local types = require("./types") + +local function compareModules(lhs: types.Module, rhs: types.Module) + return lhs.name < rhs.name +end + +return compareModules diff --git a/.lute/lib/monorepo/configure.luau b/.lute/lib/monorepo/configure.luau new file mode 100644 index 0000000..081e8bf --- /dev/null +++ b/.lute/lib/monorepo/configure.luau @@ -0,0 +1,92 @@ +local Boba = require("@modules/boba") + +local function newStringLiteral(str: T | ""): Boba.Boba + return Boba.Literal(str) +end + +local ModuleName = newStringLiteral("api") + :Or(newStringLiteral("boba")) + :Or(newStringLiteral("std")) + :Or(newStringLiteral("types")) + :Nickname("ModuleName") + +local ModuleConfig = Boba.Struct { + name = ModuleName.inner, + description = Boba.String.inner, + + repository = Boba.String:Optional().inner, + documentation = Boba.String:Optional().inner, + + authors = Boba.String:Array():Optional().inner, + license = Boba.String:Optional().inner, + version = Boba.String:Optional().inner, + private = Boba.Boolean:Optional().inner, + + dependencies = ModuleName:Map(Boba.Boolean):Optional().inner, +}:Nickname("ModuleConfig") + +local ModuleRegistry = Boba.Struct { + scope = Boba.String.inner, + registry = Boba.String:Optional().inner, + moduleFormat = Boba.String:Optional().inner, +}:Nickname("ModuleRegistry") + +local RepositoryConfig = Boba.Struct { + metadata = Boba.Struct { + repository = Boba.String.inner, + documentation = Boba.String.inner, + + authors = Boba.String:Array().inner, + license = Boba.String.inner, + version = Boba.String.inner, + copyright = Boba.String.inner, + }.inner, + + modules = Boba.Struct { + dir = Boba.String.inner, + + registries = Boba.Struct { + pesde = ModuleRegistry:Optional().inner, + wally = ModuleRegistry:Optional().inner, + npm = ModuleRegistry:Optional().inner, + } + :Optional().inner, + }.inner, + + build = Boba.Struct { + src = Boba.String.inner, + extensions = Boba.String.inner, + + output = Boba.String.inner, + model = Boba.String.inner, + project = Boba.String.inner, + }.inner, + + sourcemap = Boba.Struct { + filename = Boba.String.inner, + project = Boba.String.inner, + }.inner, +}:Nickname("RepositoryConfig") + +export type ModuleName = typeof(ModuleName.inner) +export type ModuleConfig = typeof(ModuleConfig.inner) +export type ModuleRegistry = typeof(ModuleRegistry.inner) +export type RepositoryConfig = typeof(RepositoryConfig.inner) + +local function module(x: ModuleConfig) + return ModuleConfig:assert(x) +end + +local function repository(x: RepositoryConfig) + return RepositoryConfig:assert(x) +end + +return table.freeze({ + ModuleName = ModuleName, + ModuleConfig = ModuleConfig, + ModuleRegistry = ModuleRegistry, + RepositoryConfig = RepositoryConfig, + + module = module, + repository = repository, +}) diff --git a/.lute/lib/monorepo/types.luau b/.lute/lib/monorepo/types.luau new file mode 100644 index 0000000..1c0ba6d --- /dev/null +++ b/.lute/lib/monorepo/types.luau @@ -0,0 +1,18 @@ +local configure = require("@monorepo/configure") +local path = require("@std/path") + +export type Module = { + name: string, + relativePath: path.pathlike, + configPath: path.pathlike, + configRequirePath: path.pathlike, + config: configure.ModuleConfig, +} + +export type ModuleResult = { + names: { string }, + modules: { Module }, + nameToModule: { [string]: Module }, +} + +return nil diff --git a/.lute/lib/monorepo/useForwardedModules.luau b/.lute/lib/monorepo/useForwardedModules.luau new file mode 100644 index 0000000..a42d9a7 --- /dev/null +++ b/.lute/lib/monorepo/useForwardedModules.luau @@ -0,0 +1,33 @@ +local compareModules = require("./compareModules") +local process = require("@lute/process") +local types = require("./types") + +local function useForwardedModules(modules: types.ModuleResult, forwarded: { string }?): { types.Module } + local targetModules: { types.Module } + + if forwarded then + local countForwarded = #forwarded + if countForwarded > 0 then + targetModules = table.create(#forwarded) :: { types.Module } + for _, moduleName in forwarded do + local module = modules.nameToModule[moduleName] + if module then + table.insert(targetModules, module) + continue + end + + print(`Unknown module named {moduleName}`) + print(`Valid modules: {table.concat(modules.names)}`) + return process.exit(1) + end + table.sort(targetModules, compareModules) + return targetModules + end + end + + local countModules = #modules.modules + targetModules = table.move(modules.modules, 1, countModules, 1, table.create(#modules.modules) :: any) + return targetModules +end + +return useForwardedModules diff --git a/.lute/lib/monorepo/useModules.luau b/.lute/lib/monorepo/useModules.luau new file mode 100644 index 0000000..e844226 --- /dev/null +++ b/.lute/lib/monorepo/useModules.luau @@ -0,0 +1,60 @@ +local compareModules = require("@monorepo/compareModules") +local configure = require("@monorepo/configure") +local cwdrequire = require("@lib/utils/requireCwd") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@lute/process") +local types = require("@monorepo/types") + +local function useModules(repository: configure.RepositoryConfig): types.ModuleResult + local names: { string } = {} + local modules: { types.Module } = {} + local nameToModule: { [string]: types.Module } = {} + + local modulesDir = repository.modules.dir + for _, module in fs.listdirectory(modulesDir) do + local moduleName: string = module.name + local modulePath = path.join(modulesDir, moduleName) + local moduleConfigRequirePath = path.join(modulePath, "__config__") + local moduleConfigRealPath = path.join(modulePath, "__config__.luau") + + if not fs.exists(moduleConfigRealPath) then + continue + end + + print(`Requiring module {moduleName}`, `(path: {modulePath})`) + + local requireSucess, configOrError = pcall(cwdrequire, moduleConfigRequirePath) + if not requireSucess then + print(`Failed to require configuration for module {moduleName}`, `(path: {modulePath}):`) + print(configOrError) + return process.exit(1) + end + + local configResult = configure.ModuleConfig:match(configOrError) + if configResult.ok then + local self: types.Module = { + name = moduleName, + relativePath = modulePath, + configPath = moduleConfigRealPath, + configRequirePath = moduleConfigRequirePath, + config = configOrError, + } + + table.insert(names, moduleName) + table.insert(modules, self) + nameToModule[moduleName] = self + continue + end + + print(`Failed to require configuration for module {moduleName}`, `(path: {modulePath}):`) + print(configResult:format()) + return process.exit(1) + end + + table.sort(names) + table.sort(modules, compareModules) + return { names = names, modules = modules, nameToModule = nameToModule } +end + +return useModules diff --git a/.lute/lib/monorepo/useRepository.luau b/.lute/lib/monorepo/useRepository.luau new file mode 100644 index 0000000..5af14fb --- /dev/null +++ b/.lute/lib/monorepo/useRepository.luau @@ -0,0 +1,25 @@ +local configure = require("@monorepo/configure") +local cwdrequire = require("@lib/utils/requireCwd") +local process = require("@lute/process") + +local function useRepository(maybeRepoPath: string?): configure.RepositoryConfig + local repoPath = maybeRepoPath or "__config__" + local repoRequired, repoConfigOrErrror = pcall(cwdrequire, repoPath) + + if not repoRequired then + print(`Failed to require repository config from {repoPath}:`) + print(repoConfigOrErrror) + return process.exit(1) + end + + local repoResult = configure.RepositoryConfig:match(repoConfigOrErrror) + if not repoResult.ok then + print(`Invalid repository config at {repoPath}:`) + print(repoResult:format()) + return process.exit(1) + end + + return repoConfigOrErrror +end + +return useRepository diff --git a/.lute/lib/Summon.luau b/.lute/lib/utils/Summon.luau similarity index 94% rename from .lute/lib/Summon.luau rename to .lute/lib/utils/Summon.luau index 371e4ef..9684864 100644 --- a/.lute/lib/Summon.luau +++ b/.lute/lib/utils/Summon.luau @@ -1,6 +1,5 @@ local cli = require("@batteries/cli") local process = require("@lute/process") -local richterm = require("@batteries/richterm") local stringext = require("@std/stringext") local Summon = {} @@ -62,7 +61,7 @@ end function Summon.run(self: Summon) local command = self:getCommand() - print(richterm.black(`> {command}`)) + print(`> {command}`) return process.system(command, { cwd = self.cwd, env = self.env, @@ -104,4 +103,8 @@ function Summon.codeFromMany(...: Summon) return code end +function Summon.exitFromMany(...: Summon) + return process.exit((Summon.codeFromMany :: any)(...)) +end + return table.freeze(Summon) diff --git a/.lute/lib/utils/copyInto.luau b/.lute/lib/utils/copyInto.luau new file mode 100644 index 0000000..edf65a2 --- /dev/null +++ b/.lute/lib/utils/copyInto.luau @@ -0,0 +1,25 @@ +local fs = require("@std/fs") +local path = require("@std/path") + +local function copyInto(from: path.path, to: path.path) + if fs.type(from) == "dir" then + if not fs.exists(to) then + fs.createdirectory(to) + end + + for _, entry in fs.listdirectory(from) do + local entryPath = path.join(from, entry.name) + local entryDestination = path.join(to, entry.name) + + if entry.type == "dir" then + copyInto(entryPath, entryDestination) + else + fs.copy(entryPath, entryDestination) + end + end + else + fs.copy(from, to) + end +end + +return copyInto diff --git a/.lute/lib/utils/requireCwd.luau b/.lute/lib/utils/requireCwd.luau new file mode 100644 index 0000000..67a1e73 --- /dev/null +++ b/.lute/lib/utils/requireCwd.luau @@ -0,0 +1,7 @@ +local path = require("@std/path") + +local function requireCwd(modulePath: string) + return (require)(path.format(path.join("..", "..", "..", modulePath))) +end + +return requireCwd diff --git a/.lute/setup-lute-typedefs.luau b/.lute/setup-lute-typedefs.luau new file mode 100644 index 0000000..9d0233e --- /dev/null +++ b/.lute/setup-lute-typedefs.luau @@ -0,0 +1,25 @@ +-- This script copies Lute's type definitions to the repository's +-- `.lute/typedefs` directory for the LSP. + +local Summon = require("@lib/utils/Summon") +local copyInto = require("@lib/utils/copyInto") +local fs = require("@std/fs") +local path = require("@std/path") +local process = require("@lute/process") + +local LUTE_VERSION = "0.1.0" +local SRC_BASE_PATH = path.parse(`.lute/typedefs/{LUTE_VERSION}`) +local SRC_PATH = path.join(process.homedir(), SRC_BASE_PATH) +local DESTINATION_PATH = path.join(process.cwd(), ".lute", "typedefs") + +if fs.exists(DESTINATION_PATH) then + for _, entry in fs.listdirectory(DESTINATION_PATH) do + if entry.type == "dir" then + fs.removedirectory(path.join(DESTINATION_PATH, entry.name), { recursive = true }) + end + end +else + Summon.new("lute", "setup"):assert() +end + +copyInto(SRC_PATH, DESTINATION_PATH) diff --git a/.lute/setup.luau b/.lute/setup.luau new file mode 100644 index 0000000..5492e6e --- /dev/null +++ b/.lute/setup.luau @@ -0,0 +1,4 @@ +local Summon = require("@lib/utils/Summon") +local process = require("@lute/process") + +process.exit(Summon.codeFromMany(Summon.new("lute", "setup-lute-typedefs"))) diff --git a/.lute/typedefs/.luaurc b/.lute/typedefs/.luaurc new file mode 100644 index 0000000..0673218 --- /dev/null +++ b/.lute/typedefs/.luaurc @@ -0,0 +1,3 @@ +{ + "languageMode": "nonstrict" +} diff --git a/.lute/validate-configs.luau b/.lute/validate-configs.luau new file mode 100644 index 0000000..3c62a6c --- /dev/null +++ b/.lute/validate-configs.luau @@ -0,0 +1,22 @@ +local cli = require("@batteries/cli") +local process = require("@lute/process") +local useModules = require("@monorepo/useModules") +local useRepository = require("@monorepo/useRepository") + +local args = cli.parser() +args:add("repoconfig", "option", { help = "Path from the CWD to repository config" }) +args:add("help", "flag", { help = "Shows help" }) + +args:parse({ ... }) + +if args:has("help") then + print("Sanity checks Rocket's config files") + args:help() + process.exit(0) + return +else + useModules(useRepository(args:get("repoconfig"))) + print("All modules are valid") +end + +process.exit(0) diff --git a/.lute/watch-docs.luau b/.lute/watch-docs.luau deleted file mode 100644 index cb97778..0000000 --- a/.lute/watch-docs.luau +++ /dev/null @@ -1,5 +0,0 @@ -local Summon = require("@lib/Summon") -local path = require("@std/path") -local process = require("@lute/process") - -Summon.new("bun", "run", "dev"):withCwd(path.format(path.join(process.cwd(), "docs"))):assert() diff --git a/.lute/watch.luau b/.lute/watch.luau deleted file mode 100644 index 19365e5..0000000 --- a/.lute/watch.luau +++ /dev/null @@ -1,96 +0,0 @@ -local config = require("@lib/config")() -local fs = require("@lute/fs") -local path = require("@std/path") -local process = require("@lute/process") -local richterm = require("@batteries/richterm") -local task = require("@lute/task") - -local commandArgs = { "lute", "build", "plugin" } -table.move({ ... }, 2, select("#", ...), #commandArgs + 1, commandArgs) - -local cwd = process.cwd() - -local watchers: { [string]: fs.WatchHandle } = {} -local knownEntries, numFiles, numDirs = {} :: { [string]: true }, 0, 0 -local needsRebuild, isRebuilding = false, false -local lastRebuiltAt = os.time() -local watchRocket - -local function clearOutput() - print((string.rep("\n", 120))) -end - -local function printStatus(color: richterm.Formatter, status: string) - local out = ` ⋅ Last rebuilt at {os.date("%H:%M:%S", lastRebuiltAt)}` - out ..= ` ⋅ Watching {numFiles} files and {numDirs} directories` - out = richterm.bold(color("→ " .. status)) .. richterm.gray(out) - print(out) - print() -end - -local function setRebuildNeeded() - needsRebuild = true -end - -local function rebuild() - if isRebuilding then - return - end - - isRebuilding = true - needsRebuild = false - lastRebuiltAt = os.time() - - clearOutput() - printStatus(richterm.cyan, "Rebuilding...") - - local result = process.run(commandArgs) - clearOutput() - print((string.gsub(result.stdout .. result.stderr, "\\n$", ""))) - if result.ok then - printStatus(richterm.green, "Build success!") - else - printStatus(richterm.red, "Build failed!") - end - watchRocket() - - isRebuilding = false -end - -local function watchRecursive(dir: string) - for _, entry in fs.listdir(dir) do - local fullPath = path.format(path.join(dir, entry.name)) - - -- TODO: removed files - if not knownEntries[fullPath] then - knownEntries[fullPath] = true - setRebuildNeeded() - end - - if entry.type ~= "dir" then - numFiles += 1 - continue - end - - if watchers[fullPath] == nil then - watchers[fullPath] = fs.watch(fullPath, setRebuildNeeded) - numDirs += 1 - end - - watchRecursive(fullPath) - end -end - -function watchRocket() - watchRecursive(path.format(path.join(cwd, config.build.src))) - watchRecursive(path.format(path.join(cwd, config.build.extensions))) -end - -do - watchRocket() - while task.wait(0.1) do - if needsRebuild then - rebuild() - end - end -end diff --git a/.zed/settings.json b/.zed/settings.json index e0f8031..f665458 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -5,7 +5,7 @@ { "project_name": "Rocket", "file_types": { - "JSONC": [".luaurc"] + "JSONC": [".luaurc"], }, "file_scan_exclusions": [ "**/.git", @@ -18,17 +18,18 @@ "**/.classpath", "**/.settings", ".git", - "out", + "luau_packages", + "lune_packages", "roblox.d.luau", "Rocket.rbxm", - "pesde.lock" + "pesde.lock", ], "languages": { "Luau": { "formatter": { - "external": { "command": "stylua", "arguments": ["-"] } - } - } + "external": { "command": "stylua", "arguments": ["-"] }, + }, + }, }, "lsp": { "json-language-server": { @@ -37,15 +38,15 @@ "schemas": [ { "fileMatch": [".luaurc"], - "url": "https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/refs/heads/main/editors/code/schemas/luaurc.json" + "url": "https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/refs/heads/main/editors/code/schemas/luaurc.json", }, { "fileMatch": ["*.project.json"], - "url": "https://raw.githubusercontent.com/rojo-rbx/vscode-rojo/refs/heads/master/schemas/project.template.schema.json" - } - ] - } - } + "url": "https://raw.githubusercontent.com/rojo-rbx/vscode-rojo/refs/heads/master/schemas/project.template.schema.json", + }, + ], + }, + }, }, "luau-lsp": { "settings": { @@ -59,21 +60,21 @@ "suggestRequires": true, "requireStyle": "Auto", "stringRequires": { - "enabled": true + "enabled": true, }, "separateGroupsWithLine": true, - "ignoreGlobs": ["**/.pesde/**", "**/out/**"] - } - } + "ignoreGlobs": ["**/.pesde/**", "**/out/**"], + }, + }, }, "roblox": { "enabled": true, - "security_level": "plugin" + "security_level": "plugin", }, "fflags": { - "enable_new_solver": true - } - } - } - } + "enable_new_solver": true, + }, + }, + }, + }, } diff --git a/__config__.luau b/__config__.luau new file mode 100644 index 0000000..cb8f566 --- /dev/null +++ b/__config__.luau @@ -0,0 +1,36 @@ +local configure = require("@monorepo/configure") + +return configure.repository { + metadata = { + repository = "https://github.com/teamfireworks/rocket", + documentation = "https://rocket.luau.page", + + authors = { "Team Fireworks" }, + license = "MPL-2.0", + version = "0.0.0", + copyright = "Copyright (c) 2025–2026 The Rocket Authors", + }, + + modules = { + dir = "modules", + registries = { + npm = { scope = "rbxts", moduleFormat = "rocket-%s" }, + pesde = { scope = "team_fireworks", moduleFormat = "rocket_%s" }, + wally = { scope = "team-fireworks", moduleFormat = "rocket-%s" }, + }, + }, + + build = { + src = "src", + extensions = "extensions", + + output = "out", + project = "build.project.json", + model = "Rocket.rbxm", + }, + + sourcemap = { + filename = "sourcemap.json", + project = "default.project.json", + }, +} diff --git a/modules/api/__config__.luau b/modules/api/__config__.luau new file mode 100644 index 0000000..f883e54 --- /dev/null +++ b/modules/api/__config__.luau @@ -0,0 +1,10 @@ +local configure = require("@monorepo/configure") + +return configure.module { + name = "api", + description = "Wraps the Rocket Plugin's Extension API with types", + + dependencies = { + types = true, + }, +} diff --git a/modules/api/init.luau b/modules/api/init.luau index 6b3acf3..e65ace1 100644 --- a/modules/api/init.luau +++ b/modules/api/init.luau @@ -1,4 +1,4 @@ -local types = require("./api-types") +local types = require("./types") export type FromExtension = types.FromExtension @@ -38,5 +38,18 @@ export type GetIrisWidget = types.GetIrisWidget export type MergeTable = types.MergeTable export type IntersectionToTable = types.IntersectionToTable -local Rocket: types.Rocket = (require)(game:WaitForChild("Rocket")) +--[=[ + @class Rocket + @since 0.0.0 + + The Rocket extension library. + + See the `types` package for more information. + + ```lua + local Rocket = require(plugin.Packages.Rocket) + ``` +]=] +local Rocket: types.Rocket = (require)(game:WaitForChild("Rocket", math.huge)) + return Rocket diff --git a/modules/boba/__config__.luau b/modules/boba/__config__.luau new file mode 100644 index 0000000..dc5d123 --- /dev/null +++ b/modules/boba/__config__.luau @@ -0,0 +1,6 @@ +local configure = require("@monorepo/configure") + +return configure.module { + name = "boba", + description = "Enabling the Rocket plugin to use the Boba typechecker", +} diff --git a/include/boba-base.luau b/modules/boba/base.luau similarity index 71% rename from include/boba-base.luau rename to modules/boba/base.luau index 3d562b5..2035cb3 100644 --- a/include/boba-base.luau +++ b/modules/boba/base.luau @@ -1,15 +1,13 @@ --!nonstrict --!optimize 2 --- Modified for new solver usage in Rocket --- TODO: Boba for new solver? --[[ ▄ █ ▀█▀ █ ▄ Welcome To Hell @ wthrblx.com █▀█ █ █▀█ (c) Team Fireworks 2024-2026. This software is provided 'as-is', without any express or implied - warranty. In no event will the contributors be held liable for any damages + warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, @@ -109,6 +107,7 @@ Boba.quoteGot = quoteGot export type BobaInner = { expectedType: string, match: (self: Boba, x: unknown) -> Result, + T: T, inner: T, } @@ -119,6 +118,7 @@ function Boba.new(expectedType: string, match: (self: Boba, x: any) -> Res expectedType = expectedType, match = match, }, Boba) + self.T = self :: any self.inner = self :: any return self end @@ -163,7 +163,7 @@ function Boba.Untype(self: Boba): Boba end function Boba.And(left: Boba, right: Boba): Boba - return Boba.new(`({left.expectedType}) & ({right.expectedType})`, function(self: Boba, x: any) + return Boba.new(`({left.expectedType}) & ({right.expectedType})`, function(self, x) local leftResult = left:match(x) if leftResult.ok then local rightResult = right:match(x) @@ -176,7 +176,7 @@ function Boba.And(left: Boba, right: Boba): Boba end function Boba.Or(left: Boba, right: Boba): Boba - return Boba.new(`{left.expectedType} | {right.expectedType}`, function(self: Boba, x: any) + return Boba.new(`{left.expectedType} | {right.expectedType}`, function(self, x) local leftResult = left:match(x) local rightResult = right:match(x) if leftResult.ok or rightResult.ok then @@ -189,7 +189,7 @@ function Boba.Or(left: Boba, right: Boba): Boba end function Boba.Optional(inner: Boba): Boba - return Boba.new(`({inner.expectedType})?`, function(_: Boba, x: any) + return Boba.new(`({inner.expectedType})?`, function(_, x) if x == nil then return Result.ok end @@ -198,7 +198,7 @@ function Boba.Optional(inner: Boba): Boba end function Boba.Predicate(inner: Boba, predicate: (value: T) -> (boolean, string?)): Boba - return Boba.new(`Predicate<{inner.expectedType}>`, function(self: Boba, x: any) + return Boba.new(`Predicate<{inner.expectedType}>`, function(self, x) local innerResult = inner:match(x) if innerResult.ok then @@ -215,19 +215,19 @@ function Boba.Predicate(inner: Boba, predicate: (value: T) -> (boolean, st end function Boba.Literal(literal: T, nickname: string?): Boba - return Boba.new(nickname or tostring(literal), function(self: Boba, x: any) + return Boba.new(nickname or tostring(literal), function(self, x) return if rawequal(x, literal) then Result.ok else self:fail(`only {literal} is accepted, but {quoteGot(x)}`) end) end function Boba.Typeof(expected: string) - return Boba.new(expected, function(self: Boba, x: any) + return Boba.new(expected, function(self, x) return if typeof(x) == expected then Result.ok else self:fail(quoteGot(x)) end) end function Boba.Array(values: Boba): Boba<{ V }> - return Boba.new(`\{ {values.expectedType} \}`, function(self: Boba, x: any) + return Boba.new(`\{ {values.expectedType} \}`, function(self, x) if typeof(x) ~= "table" then return self:fail(`expected a table, but {quoteGot(x)}`) end @@ -253,7 +253,7 @@ function Boba.Array(values: Boba): Boba<{ V }> end function Boba.Map(keys: Boba, values: Boba): Boba<{ [K]: V }> - return Boba.new(`\{ [{keys.expectedType}]: {values.expectedType} \}`, function(self: Boba, x: any) + return Boba.new(`\{ [{keys.expectedType}]: {values.expectedType} \}`, function(self, x) if typeof(x) ~= "table" then return self:fail(`expected a table, but {quoteGot(x)}`) end @@ -275,7 +275,7 @@ function Boba.Map(keys: Boba, values: Boba): Boba<{ [K]: V }> end function Boba.Set(keys: Boba): Boba<{ [K]: true }> - return Boba.new(`\{ [{keys.expectedType}]: true \}`, function(self: Boba, x: any) + return Boba.new(`\{ [{keys.expectedType}]: true \}`, function(self, x) if typeof(x) ~= "table" then return self:fail(`expected a table, but {quoteGot(x)}`) end @@ -295,14 +295,14 @@ function Boba.Set(keys: Boba): Boba<{ [K]: true }> end) end -function Boba.Struct(struct: T): Boba +function Boba.Struct(struct: T & { [string]: Boba }): Boba local expectedPairs = { "[any]: any" } for key, value in struct :: { [string]: Boba } do -- TODO: key could be better formatted here table.insert(expectedPairs, `{key}: {value.expectedType}`) end - return Boba.new(`\{ {table.concat(expectedPairs, ", ")} \}`, function(self: Boba, x: any) + return Boba.new(`\{ {table.concat(expectedPairs, ", ")} \}`, function(self, x) if typeof(x) ~= "table" then return self:fail(`expected a table, but {quoteGot(x)}`) end @@ -326,7 +326,7 @@ function Boba.ExhaustiveStruct(struct: T): Boba table.insert(expectedPairs, `{key}: {value.expectedType}`) end - return Boba.new(`\{ {table.concat(expectedPairs, ", ")} \}`, function(self: Boba, x: any) + return Boba.new(`\{ {table.concat(expectedPairs, ", ")} \}`, function(self, x) if typeof(x) ~= "table" then return self:fail(`expected a table, but {quoteGot(x)}`) end @@ -349,6 +349,67 @@ function Boba.ExhaustiveStruct(struct: T): Boba end) end +local function Function(...: Boba) + local arguments = table.pack(...) + return function(...: Boba) + local returns = table.pack(...) + + local argumentsFormatted, returnsFormatted = {}, {} + for index = 1, arguments.n do + argumentsFormatted[index] = arguments[index].expectedType + end + + for index = 1, returns.n do + returnsFormatted[index] = returns[index].expectedType + end + + local expectedReturns = table.concat(argumentsFormatted, ", ") + if returns.n ~= 1 then + expectedReturns = "(" .. expectedReturns .. ")" + end + + return Boba.new(`({table.concat(argumentsFormatted, ", ")}) -> {expectedReturns}`, function(self, x) + if typeof(x) == "function" then + return Result.ok + end + + return Result.fail(self.expectedType, quoteGot(x)) + end) + end +end + +-- What the fuck Luau + +-- stylua: ignore +type FunctionReturnsConstructor = + & (() -> Boba<(Args...) -> ()>) + & ((Boba) -> Boba<(Args...) -> T1>) + & ((Boba, Boba) -> Boba<(Args...) -> (T1, T2)>) + & ((Boba, Boba, Boba) -> Boba<(Args...) -> (T1, T2, T3)>) + & ((Boba, Boba, Boba, Boba) -> Boba<(Args...) -> (T1, T2, T3, T4)>) + & ((Boba, Boba, Boba, Boba, Boba) -> Boba<(Args...) -> (T1, T2, T3, T4, T5)>) + & ((Boba, Boba, Boba, Boba, Boba, Boba) -> Boba<(Args...) -> (T1, T2, T3, T4, T5, T6)>) + & ((Boba, Boba, Boba, Boba, Boba, Boba, Boba) -> Boba<(Args...) -> (T1, T2, T3, T4, T5, T6, T7)>) + & ((Boba, Boba, Boba, Boba, Boba, Boba, Boba, Boba) -> Boba<(Args...) -> (T1, T2, T3, T4, T5, T6, T7, T8)>) + & ((Boba, Boba, Boba, Boba, Boba, Boba, Boba, Boba, Boba) -> Boba<(Args...) -> (T1, T2, T3, T4, T5, T6, T7, T8, T9)>) + & ((...Boba) -> Boba<(Args...) -> ...any>) + +-- stylua: ignore +type FunctionConstructor = + & (() -> FunctionReturnsConstructor<>) + & ((Boba) -> FunctionReturnsConstructor) + & ((Boba, Boba) -> FunctionReturnsConstructor) + & ((Boba, Boba, Boba) -> FunctionReturnsConstructor) + & ((Boba, Boba, Boba, Boba) -> FunctionReturnsConstructor) + & ((Boba, Boba, Boba, Boba, Boba) -> FunctionReturnsConstructor) + & ((Boba, Boba, Boba, Boba, Boba, Boba) -> FunctionReturnsConstructor) + & ((Boba, Boba, Boba, Boba, Boba, Boba, Boba) -> FunctionReturnsConstructor) + & ((Boba, Boba, Boba, Boba, Boba, Boba, Boba, Boba) -> FunctionReturnsConstructor) + & ((Boba, Boba, Boba, Boba, Boba, Boba, Boba, Boba, Boba) -> FunctionReturnsConstructor) + & ((...Boba) -> FunctionReturnsConstructor<...any>) + +Boba.Function = Function :: FunctionConstructor + Boba.Any = Boba.new("any", function() return Result.ok end) :: Boba @@ -363,7 +424,7 @@ end) :: Boba Boba.Boolean = Boba.Typeof("boolean") :: Boba Boba.Buffer = Boba.Typeof("buffer") :: Boba -Boba.Function = Boba.Typeof("function") :: Boba<(...any) -> ...any> +Boba.AnyFunction = Boba.Typeof("function") :: Boba<(...any) -> ...any> Boba.Number = Boba.Typeof("number") :: Boba Boba.String = Boba.Typeof("string") :: Boba Boba.Table = Boba.Typeof("table") :: Boba<{ [any]: any }> diff --git a/modules/boba/init.luau b/modules/boba/init.luau new file mode 100644 index 0000000..cbaea8a --- /dev/null +++ b/modules/boba/init.luau @@ -0,0 +1,1035 @@ +--!strict +--!optimize 2 +-- stylua: ignore start + +--[[ + + ▄ █ ▀█▀ █ ▄ Welcome To Hell @ wthrblx.com + █▀█ █ █▀█ (c) Team Fireworks 2024-2026. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the contributors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +]] + +local Boba = require("@self/base") +local Result, quoteGot = Boba.Result, Boba.quoteGot + +export type Boba = Boba.Boba +export type Result = Boba.Result + +local ext = {} + +-- boba: start roblox data types codegen +ext.Axes = Boba.Typeof("Axes") :: Boba +ext.BrickColor = Boba.Typeof("BrickColor") :: Boba +ext.CatalogSearchParams = Boba.Typeof("CatalogSearchParams") :: Boba +ext.CFrame = Boba.Typeof("CFrame") :: Boba +ext.Color3 = Boba.Typeof("Color3") :: Boba +ext.ColorSequence = Boba.Typeof("ColorSequence") :: Boba +ext.ColorSequenceKeypoint = Boba.Typeof("ColorSequenceKeypoint") :: Boba +ext.Content = Boba.Typeof("Content") :: Boba +ext.DateTime = Boba.Typeof("DateTime") :: Boba +ext.DockWidgetPluginGuiInfo = Boba.Typeof("DockWidgetPluginGuiInfo") :: Boba +ext.Enum = Boba.Typeof("Enum") :: Boba +ext.EnumItem = Boba.Typeof("EnumItem") :: Boba +ext.Enums = Boba.Typeof("Enums") :: Boba +ext.Faces = Boba.Typeof("Faces") :: Boba +ext.FloatCurveKey = Boba.Typeof("FloatCurveKey") :: Boba +ext.Font = Boba.Typeof("Font") :: Boba +ext.Instance = Boba.Typeof("Instance") :: Boba +ext.NumberRange = Boba.Typeof("NumberRange") :: Boba +ext.NumberSequence = Boba.Typeof("NumberSequence") :: Boba +ext.NumberSequenceKeypoint = Boba.Typeof("NumberSequenceKeypoint") :: Boba +ext.OverlapParams = Boba.Typeof("OverlapParams") :: Boba +ext.Path2DControlPoint = Boba.Typeof("Path2DControlPoint") :: Boba +ext.PathWaypoint = Boba.Typeof("PathWaypoint") :: Boba +ext.PhysicalProperties = Boba.Typeof("PhysicalProperties") :: Boba +ext.Random = Boba.Typeof("Random") :: Boba +ext.Ray = Boba.Typeof("Ray") :: Boba +ext.RaycastParams = Boba.Typeof("RaycastParams") :: Boba +ext.RaycastResult = Boba.Typeof("RaycastResult") :: Boba +ext.RBXScriptConnection = Boba.Typeof("RBXScriptConnection") :: Boba +ext.RBXScriptSignal = Boba.Typeof("RBXScriptSignal") :: Boba +ext.Rect = Boba.Typeof("Rect") :: Boba +ext.Region3 = Boba.Typeof("Region3") :: Boba +ext.Region3int16 = Boba.Typeof("Region3int16") :: Boba +ext.RotationCurveKey = Boba.Typeof("RotationCurveKey") :: Boba +ext.Secret = Boba.Typeof("Secret") :: Boba +ext.SharedTable = Boba.Typeof("SharedTable") :: Boba +ext.TweenInfo = Boba.Typeof("TweenInfo") :: Boba +ext.UDim = Boba.Typeof("UDim") :: Boba +ext.UDim2 = Boba.Typeof("UDim2") :: Boba +ext.ValueCurveKey = Boba.Typeof("ValueCurveKey") :: Boba +ext.Vector2 = Boba.Typeof("Vector2") :: Boba +ext.Vector2int16 = Boba.Typeof("Vector2int16") :: Boba +ext.Vector3 = Boba.Typeof("Vector3") :: Boba +ext.Vector3int16 = Boba.Typeof("Vector3int16") :: Boba +-- boba: end roblox data types codegen + +local function Tree(parentOrChildren: any, maybeChildren: any): Boba + local parent: Boba = if maybeChildren then parentOrChildren else ext.Instance :: any + local children = maybeChildren or parentOrChildren :: any + + local expectedPairs: { string } = { "[string]: Instance" } + for childName, childBoba: Boba in (children :: any) do + table.insert(expectedPairs, `{childName}: {childBoba.expectedType}`) + end + + return Boba.new(`{parent.expectedType} & \{ {table.concat(expectedPairs, ", ")} \}`, function(self, x) + local parentResult = parent:match(x) + if not parentResult.ok then + return self:fail("as it didn't match the parent type"):because(parentResult) + end + + for childName: string, childBoba: Boba in (children :: any) do + local realInstance = (x :: Instance):FindFirstChild(childName) + local childResult = childBoba:match(realInstance) + if not childResult.ok then + return self:fail(`didn't match child "{childName}"`):because(childResult) + end + end + + return Result.ok + end :: any) +end + +local function ExhaustiveTree(parentOrChildren: any, maybeChildren: any): Boba + local parent: Boba = if maybeChildren then parentOrChildren else ext.Instance :: any + local children: any = maybeChildren or parentOrChildren :: any + + local childParts: { string } = {} + for childName, childBoba: Boba in (children :: any) do + table.insert(childParts, `{childName}: {childBoba.expectedType}`) + end + + return Boba.new(`{parent.expectedType} & \{ {table.concat(childParts, ", ")} \}`, function(self, x): Result + local parentResult = parent:match(x) + if not parentResult.ok then + return self:fail("as it didn't match the parent type"):because(parentResult) :: Result + end + + for childName: string, childBoba: Boba in (children :: any) do + local realInstance = (x :: Instance):FindFirstChild(childName) + local childResult = childBoba:match(realInstance) + if not childResult.ok then + return self:fail(`didn't match child "{childName}"`):because(childResult) :: Result + end + end + + for _, instance in (x :: Instance):GetChildren() do + if not (children :: any)[instance.Name] then + return self:fail(`"{instance.Name}" isn't included in exhaustive tree`) :: Result + end + end + + return Result.ok + end :: any) +end + +type Tree = ((children: C) -> Boba) & ((parent: Boba, children: C) -> Boba) + +ext.Tree = Tree :: Tree +ext.ExhaustiveTree = ExhaustiveTree :: Tree + +ext.IsA = ({} :: any) :: Instances +ext.IsClass = ({} :: any) :: Instances + +-- casting to any to disable luau-lsp metatable magic function, which breaks +-- autocomplete +setmetatable(ext.IsA :: any, table.freeze({ + __index = function(instances: Instances, className: string) + local instanceBoba = Boba.new(className, function(self, x) + if typeof(x) == "Instance" then + if x:IsA(className) then + return Result.ok + end + return self:fail(`expected instance inherits {className}, but got {x:GetFullName()} ({x.ClassName})`) + end + return self:fail(`expected an Instance, but {quoteGot(x)}`) + end :: any); + + (instances :: any)[className] = instanceBoba + return instanceBoba + end +})) + +setmetatable(ext.IsClass :: any, table.freeze({ + __index = function(instances: Instances, className: string) + local instanceBoba = Boba.new(className, function(self, x) + if typeof(x) == "Instance" then + if x.ClassName == className then + return Result.ok + end + return self:fail(`expected instance inherits {className}, but got {x:GetFullName()} ({x.ClassName})`) + end + return self:fail(`expected an Instance, but {quoteGot(x)}`) + end :: any); + + (instances :: any)[className] = instanceBoba + return instanceBoba + end +})) + +-- boba: start roblox instances typegen +type Instances = { + Accessory: Boba, + AccessoryDescription: Boba, + AccountService: Boba, + Accoutrement: Boba, + AchievementService: Boba, + ActivityHistoryEventService: Boba, + Actor: Boba, + AdGui: Boba, + AdPortal: Boba, + AdService: Boba, + AdvancedDragger: Boba, + AirController: Boba, + AlignOrientation: Boba, + AlignPosition: Boba, + AnalyticsService: Boba, + AngularVelocity: Boba, + Animation: Boba, + AnimationClip: Boba, + AnimationClipProvider: Boba, + AnimationConstraint: Boba, + AnimationController: Boba, + AnimationFromVideoCreatorService: Boba, + AnimationFromVideoCreatorStudioService: Boba, + AnimationImportData: Boba, + AnimationRigData: Boba, + AnimationStreamTrack: Boba, + AnimationTrack: Boba, + Animator: Boba, + Annotation: Boba, + AnnotationsService: Boba, + AppLifecycleObserverService: Boba, + AppRatingPromptService: Boba, + AppStorageService: Boba, + AppUpdateService: Boba, + ArcHandles: Boba, + AssetCounterService: Boba, + AssetDeliveryProxy: Boba, + AssetImportService: Boba, + AssetImportSession: Boba, + AssetManagerService: Boba, + AssetPatchSettings: Boba, + AssetService: Boba, + AssetSoundEffect: Boba, + Atmosphere: Boba, + AtmosphereSensor: Boba, + Attachment: Boba, + AudioAnalyzer: Boba, + AudioChannelMixer: Boba, + AudioChannelSplitter: Boba, + AudioChorus: Boba, + AudioCompressor: Boba, + AudioDeviceInput: Boba, + AudioDeviceOutput: Boba, + AudioDistortion: Boba, + AudioEcho: Boba, + AudioEmitter: Boba, + AudioEqualizer: Boba, + AudioFader: Boba, + AudioFilter: Boba, + AudioFlanger: Boba, + AudioFocusService: Boba, + AudioGate: Boba, + AudioLimiter: Boba, + AudioListener: Boba, + AudioPages: Boba, + AudioPitchShifter: Boba, + AudioPlayer: Boba, + AudioRecorder: Boba, + AudioReverb: Boba, + AudioSearchParams: Boba, + AudioSpeechToText: Boba, + AudioTextToSpeech: Boba, + AudioTremolo: Boba, + AuroraScriptObject: Boba, + AvatarAccessoryRules: Boba, + AvatarAnimationRules: Boba, + AvatarBodyRules: Boba, + AvatarChatService: Boba, + AvatarClothingRules: Boba, + AvatarCollisionRules: Boba, + AvatarCreationService: Boba, + AvatarEditorService: Boba, + AvatarImportService: Boba, + AvatarRules: Boba, + AvatarSettings: Boba, + Backpack: Boba, + BackpackItem: Boba, + BadgeService: Boba, + BallSocketConstraint: Boba, + BanHistoryPages: Boba, + BaseImportData: Boba, + BasePart: Boba, + BasePlayerGui: Boba, + BaseRemoteEvent: Boba, + BaseScript: Boba, + BaseWrap: Boba, + Beam: Boba, + BevelMesh: Boba, + BillboardGui: Boba, + BinaryStringValue: Boba, + BindableEvent: Boba, + BindableFunction: Boba, + BlockMesh: Boba, + BloomEffect: Boba, + BlurEffect: Boba, + BodyAngularVelocity: Boba, + BodyColors: Boba, + BodyForce: Boba, + BodyGyro: Boba, + BodyMover: Boba, + BodyPartDescription: Boba, + BodyPosition: Boba, + BodyThrust: Boba, + BodyVelocity: Boba, + Bone: Boba, + BoolValue: Boba, + BoxHandleAdornment: Boba, + Breakpoint: Boba, + BrickColorValue: Boba, + BrowserService: Boba, + BubbleChatConfiguration: Boba, + BubbleChatMessageProperties: Boba, + BugReporterService: Boba, + BulkImportService: Boba, + BuoyancySensor: Boba, + CFrameValue: Boba, + CSGDictionaryService: Boba, + CacheableContentProvider: Boba, + CalloutService: Boba, + Camera: Boba, + CanvasGroup: Boba, + Capture: Boba, + CaptureService: Boba, + CapturesPages: Boba, + CatalogPages: Boba, + ChangeHistoryService: Boba, + ChangeHistoryStreamingService: Boba, + ChannelSelectorSoundEffect: Boba, + ChannelTabsConfiguration: Boba, + CharacterAppearance: Boba, + CharacterMesh: Boba, + Chat: Boba, + ChatInputBarConfiguration: Boba, + ChatWindowConfiguration: Boba, + ChatWindowMessageProperties: Boba, + ChatbotUIService: Boba, + ChorusSoundEffect: Boba, + ClickDetector: Boba, + ClientReplicator: Boba, + ClimbController: Boba, + Clothing: Boba, + CloudCRUDService: Boba, + CloudLocalizationTable: Boba, + Clouds: Boba, + ClusterPacketCache: Boba, + Collaborator: Boba, + CollaboratorsService: Boba, + CollectionService: Boba, + Color3Value: Boba, + ColorCorrectionEffect: Boba, + ColorGradingEffect: Boba, + CommerceService: Boba, + CompressorSoundEffect: Boba, + ConeHandleAdornment: Boba, + ConfigService: Boba, + ConfigSnapshot: Boba, + Configuration: Boba, + ConfigureServerService: Boba, + ConnectivityService: Boba, + Constraint: Boba, + ContentProvider: Boba, + ContextActionService: Boba, + Controller: Boba, + ControllerBase: Boba, + ControllerManager: Boba, + ControllerPartSensor: Boba, + ControllerSensor: Boba, + ControllerService: Boba, + ConversationalAIAcceptanceService: Boba, + CookiesService: Boba, + CoreGui: Boba, + CorePackages: Boba, + CoreScript: Boba, + CoreScriptDebuggingManagerHelper: Boba, + CoreScriptSyncService: Boba, + CornerWedgePart: Boba, + CreationDBService: Boba, + CreatorStoreService: Boba, + CrossDMScriptChangeListener: Boba, + CurveAnimation: Boba, + CustomEvent: Boba, + CustomEventReceiver: Boba, + CustomLog: Boba, + CustomSoundEffect: Boba, + CylinderHandleAdornment: Boba, + CylinderMesh: Boba, + CylindricalConstraint: Boba, + DataModel: Boba, + DataModelMesh: Boba, + DataModelPatchService: Boba, + DataModelSession: Boba, + DataStore: Boba, + DataStoreGetOptions: Boba, + DataStoreIncrementOptions: Boba, + DataStoreInfo: Boba, + DataStoreKey: Boba, + DataStoreKeyInfo: Boba, + DataStoreKeyPages: Boba, + DataStoreListingPages: Boba, + DataStoreObjectVersionInfo: Boba, + DataStoreOptions: Boba, + DataStorePages: Boba, + DataStoreService: Boba, + DataStoreSetOptions: Boba, + DataStoreVersionPages: Boba, + Debris: Boba, + DebugSettings: Boba, + DebuggablePluginWatcher: Boba, + DebuggerBreakpoint: Boba, + DebuggerConnection: Boba, + DebuggerConnectionManager: Boba, + DebuggerLuaResponse: Boba, + DebuggerManager: Boba, + DebuggerUIService: Boba, + DebuggerVariable: Boba, + DebuggerWatch: Boba, + Decal: Boba, + DepthOfFieldEffect: Boba, + DeviceIdService: Boba, + Dialog: Boba, + DialogChoice: Boba, + DistortionSoundEffect: Boba, + DockWidgetPluginGui: Boba, + DoubleConstrainedValue: Boba, + DraftsService: Boba, + DragDetector: Boba, + Dragger: Boba, + DraggerService: Boba, + DynamicRotate: Boba, + EchoSoundEffect: Boba, + EditableImage: Boba, + EditableMesh: Boba, + EditableService: Boba, + EmotesPages: Boba, + EqualizerSoundEffect: Boba, + EulerRotationCurve: Boba, + EventIngestService: Boba, + ExampleV2Service: Boba, + ExecutedRemoteCommand: Boba, + ExperienceAuthService: Boba, + ExperienceInviteOptions: Boba, + ExperienceNotificationService: Boba, + ExperienceService: Boba, + ExperienceStateCaptureService: Boba, + ExperienceStateRecordingService: Boba, + ExplorerFilter: Boba, + ExplorerFilterAutocompleter: Boba, + ExplorerServiceVisibilityService: Boba, + Explosion: Boba, + FaceAnimatorService: Boba, + FaceControls: Boba, + FaceInstance: Boba, + FacialAgeEstimationService: Boba, + FacialAnimationRecordingService: Boba, + FacialAnimationStreamingServiceStats: Boba, + FacialAnimationStreamingServiceV2: Boba, + FacialAnimationStreamingSubsessionStats: Boba, + FacsImportData: Boba, + Feature: Boba, + FeatureRestrictionManager: Boba, + File: Boba, + FileMesh: Boba, + Fire: Boba, + Flag: Boba, + FlagStand: Boba, + FlagStandService: Boba, + FlangeSoundEffect: Boba, + FloatCurve: Boba, + FloorWire: Boba, + FluidForceSensor: Boba, + FlyweightService: Boba, + Folder: Boba, + ForceField: Boba, + FormFactorPart: Boba, + Frame: Boba, + FriendPages: Boba, + FriendService: Boba, + FunctionalTest: Boba, + GamePassService: Boba, + GameSettings: Boba, + GamepadService: Boba, + GenerationService: Boba, + GenericChallengeService: Boba, + GenericSettings: Boba, + Geometry: Boba, + GeometryService: Boba, + GetTextBoundsParams: Boba, + GlobalDataStore: Boba, + GlobalSettings: Boba, + Glue: Boba, + GroundController: Boba, + GroupImportData: Boba, + GroupService: Boba, + GuiBase: Boba, + GuiBase2d: Boba, + GuiBase3d: Boba, + GuiButton: Boba, + GuiLabel: Boba, + GuiMain: Boba, + GuiObject: Boba, + GuiService: Boba, + GuidRegistryService: Boba, + HSRDataContentProvider: Boba, + HandRigDescription: Boba, + HandleAdornment: Boba, + Handles: Boba, + HandlesBase: Boba, + HapticEffect: Boba, + HapticService: Boba, + HarmonyService: Boba, + Hat: Boba, + HeapProfilerService: Boba, + HeatmapService: Boba, + HeightmapImporterService: Boba, + HiddenSurfaceRemovalAsset: Boba, + Highlight: Boba, + HingeConstraint: Boba, + Hint: Boba, + Hole: Boba, + Hopper: Boba, + HopperBin: Boba, + HttpRbxApiService: Boba, + HttpRequest: Boba, + HttpService: Boba, + Humanoid: Boba, + HumanoidController: Boba, + HumanoidDescription: Boba, + HumanoidRigDescription: Boba, + IKControl: Boba, + ILegacyStudioBridge: Boba, + IXPService: Boba, + ImageButton: Boba, + ImageHandleAdornment: Boba, + ImageLabel: Boba, + ImportSession: Boba, + IncrementalPatchBuilder: Boba, + InputAction: Boba, + InputBinding: Boba, + InputContext: Boba, + InputObject: Boba, + InsertService: Boba, + Instance: Boba, + InstanceAdornment: Boba, + InstanceExtensionsService: Boba, + IntConstrainedValue: Boba, + IntValue: Boba, + IntersectOperation: Boba, + InventoryPages: Boba, + JointImportData: Boba, + JointInstance: Boba, + JointsService: Boba, + KeyboardService: Boba, + Keyframe: Boba, + KeyframeMarker: Boba, + KeyframeSequence: Boba, + KeyframeSequenceProvider: Boba, + LSPFileSyncService: Boba, + LanguageService: Boba, + LayerCollector: Boba, + LegacyStudioBridge: Boba, + Light: Boba, + Lighting: Boba, + LineForce: Boba, + LineHandleAdornment: Boba, + LinearVelocity: Boba, + LinkingService: Boba, + LiveScriptingService: Boba, + LiveSyncService: Boba, + LocalDebuggerConnection: Boba, + LocalScript: Boba, + LocalStorageService: Boba, + LocalizationService: Boba, + LocalizationTable: Boba, + LodDataEntity: Boba, + LodDataService: Boba, + LogReporterService: Boba, + LogService: Boba, + LoginService: Boba, + LuaSettings: Boba, + LuaSourceContainer: Boba, + LuaWebService: Boba, + LuauScriptAnalyzerService: Boba, + MLModelDeliveryService: Boba, + MLService: Boba, + MLSession: Boba, + ManualGlue: Boba, + ManualSurfaceJointInstance: Boba, + ManualWeld: Boba, + MarkerCurve: Boba, + MarketplaceService: Boba, + MatchmakingService: Boba, + MaterialGenerationService: Boba, + MaterialImportData: Boba, + MaterialService: Boba, + MaterialVariant: Boba, + MemStorageConnection: Boba, + MemStorageService: Boba, + MemoryStoreHashMap: Boba, + MemoryStoreHashMapPages: Boba, + MemoryStoreQueue: Boba, + MemoryStoreService: Boba, + MemoryStoreSortedMap: Boba, + MeshContentProvider: Boba, + MeshImportData: Boba, + MeshPart: Boba, + Message: Boba, + MessageBusConnection: Boba, + MessageBusService: Boba, + MessagingService: Boba, + MetaBreakpoint: Boba, + MetaBreakpointContext: Boba, + MetaBreakpointManager: Boba, + MicroProfilerService: Boba, + Model: Boba, + ModerationService: Boba, + ModuleScript: Boba, + Motor: Boba, + Motor6D: Boba, + MotorFeature: Boba, + Mouse: Boba, + MouseService: Boba, + MultipleDocumentInterfaceInstance: Boba, + NegateOperation: Boba, + NetworkClient: Boba, + NetworkMarker: Boba, + NetworkPeer: Boba, + NetworkReplicator: Boba, + NetworkServer: Boba, + NetworkSettings: Boba, + NoCollisionConstraint: Boba, + Noise: Boba, + NonReplicatedCSGDictionaryService: Boba, + NotificationService: Boba, + NumberPose: Boba, + NumberValue: Boba, + Object: Boba, + ObjectValue: Boba, + OmniRecommendationsService: Boba, + OpenCloudApiV1: Boba, + OpenCloudService: Boba, + OperationGraph: Boba, + OrderedDataStore: Boba, + OutfitPages: Boba, + PVAdornment: Boba, + PVInstance: Boba, + PackageLink: Boba, + PackageService: Boba, + PackageUIService: Boba, + Pages: Boba, + Pants: Boba, + ParabolaAdornment: Boba, + Part: Boba, + PartAdornment: Boba, + PartOperation: Boba, + PartOperationAsset: Boba, + ParticleEmitter: Boba, + PartyEmulatorService: Boba, + PatchBundlerFileWatch: Boba, + PatchMapping: Boba, + Path: Boba, + Path2D: Boba, + PathfindingLink: Boba, + PathfindingModifier: Boba, + PathfindingService: Boba, + PausedState: Boba, + PausedStateBreakpoint: Boba, + PausedStateException: Boba, + PerformanceControlService: Boba, + PermissionsService: Boba, + PhysicsService: Boba, + PhysicsSettings: Boba, + PitchShiftSoundEffect: Boba, + PlaceAssetIdsService: Boba, + PlaceStatsService: Boba, + PlacesService: Boba, + Plane: Boba, + PlaneConstraint: Boba, + Platform: Boba, + PlatformCloudStorageService: Boba, + PlatformFriendsService: Boba, + Player: Boba, + PlayerData: Boba, + PlayerDataRecord: Boba, + PlayerDataRecordConfig: Boba, + PlayerDataService: Boba, + PlayerEmulatorService: Boba, + PlayerGui: Boba, + PlayerHydrationService: Boba, + PlayerMouse: Boba, + PlayerScripts: Boba, + PlayerViewService: Boba, + Players: Boba, + Plugin: Boba, + PluginAction: Boba, + PluginCapabilities: Boba, + PluginDebugService: Boba, + PluginDragEvent: Boba, + PluginGui: Boba, + PluginGuiService: Boba, + PluginManagementService: Boba, + PluginManager: Boba, + PluginManagerInterface: Boba, + PluginMenu: Boba, + PluginMouse: Boba, + PluginPolicyService: Boba, + PluginToolbar: Boba, + PluginToolbarButton: Boba, + PointLight: Boba, + PointsService: Boba, + PolicyService: Boba, + Pose: Boba, + PoseBase: Boba, + PostEffect: Boba, + PrismaticConstraint: Boba, + ProcessInstancePhysicsService: Boba, + ProximityPrompt: Boba, + ProximityPromptService: Boba, + PublishService: Boba, + QWidgetPluginGui: Boba, + RTAnimationTracker: Boba, + RayValue: Boba, + RbxAnalyticsService: Boba, + RecommendationPages: Boba, + RecommendationService: Boba, + ReflectionMetadata: Boba, + ReflectionMetadataCallbacks: Boba, + ReflectionMetadataClass: Boba, + ReflectionMetadataClasses: Boba, + ReflectionMetadataEnum: Boba, + ReflectionMetadataEnumItem: Boba, + ReflectionMetadataEnums: Boba, + ReflectionMetadataEvents: Boba, + ReflectionMetadataFunctions: Boba, + ReflectionMetadataItem: Boba, + ReflectionMetadataMember: Boba, + ReflectionMetadataProperties: Boba, + ReflectionMetadataYieldFunctions: Boba, + ReflectionService: Boba, + RelativeGui: Boba, + RemoteCommandService: Boba, + RemoteCursorService: Boba, + RemoteDebuggerServer: Boba, + RemoteEvent: Boba, + RemoteFunction: Boba, + RenderSettings: Boba, + RenderingTest: Boba, + ReplicatedFirst: Boba, + ReplicatedStorage: Boba, + ReverbSoundEffect: Boba, + RibbonNotificationService: Boba, + RigidConstraint: Boba, + RobloxPluginGuiService: Boba, + RobloxReplicatedStorage: Boba, + RobloxSerializableInstance: Boba, + RobloxServerStorage: Boba, + RocketPropulsion: Boba, + RodConstraint: Boba, + RomarkRbxAnalyticsService: Boba, + RomarkService: Boba, + RootImportData: Boba, + RopeConstraint: Boba, + Rotate: Boba, + RotateP: Boba, + RotateV: Boba, + RotationCurve: Boba, + RtMessagingService: Boba, + RunService: Boba, + RunningAverageItemDouble: Boba, + RunningAverageItemInt: Boba, + RunningAverageTimeIntervalItem: Boba, + RuntimeContentService: Boba, + RuntimeScriptService: Boba, + SafetyService: Boba, + ScreenGui: Boba, + ScreenshotCapture: Boba, + ScreenshotHud: Boba, + Script: Boba