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 a9e345b..30df68d 100644 --- a/.luaurc +++ b/.luaurc @@ -3,7 +3,9 @@ "aliases": { "src": "src", "include": "include", - "pkgs": "roblox_packages" + "pkgs": "roblox_packages", + "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 new file mode 100644 index 0000000..e65ace1 --- /dev/null +++ b/modules/api/init.luau @@ -0,0 +1,55 @@ +local types = require("./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 + +--[=[ + @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