From b4d4711fd582b4c22103638fbc7b7084bcb443db Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Wed, 6 May 2026 18:02:08 +0100 Subject: [PATCH 1/8] first pass --- .env.template | 21 +++ .luaurc | 12 ++ .lune/.luaurc | 6 - .lute/.config.luau | 1 - .lute/build.luau | 157 ++++++++++++++++++ .lute/lib/build-system/compileAsync.luau | 125 ++++++++++++++ .lute/lib/build-system/hashPath.luau | 48 ++++++ .lute/lib/build-system/init.luau | 10 ++ .../lib/build-system/readBuildCacheAsync.luau | 19 +++ .../lib/build-system/runBuildGroupAsync.luau | 57 +++++++ .lute/lib/build-system/types.luau | 27 +++ .../build-system/writeBuildCacheAsync.luau | 12 ++ .lute/lib/copyInto.luau | 25 +++ .lute/lib/createRojoProjectAsync.luau | 11 ++ .lute/lib/find.luau | 12 ++ .lute/lib/findWhere.luau | 29 ++++ .lute/lib/getStudioPluginsPath.luau | 13 ++ plugin/.darklua.json | 61 +++++++ project.luau | 39 +++++ rokit.toml | 1 + 20 files changed, 679 insertions(+), 7 deletions(-) create mode 100644 .env.template create mode 100644 .luaurc delete mode 100644 .lune/.luaurc create mode 100644 .lute/build.luau create mode 100644 .lute/lib/build-system/compileAsync.luau create mode 100644 .lute/lib/build-system/hashPath.luau create mode 100644 .lute/lib/build-system/init.luau create mode 100644 .lute/lib/build-system/readBuildCacheAsync.luau create mode 100644 .lute/lib/build-system/runBuildGroupAsync.luau create mode 100644 .lute/lib/build-system/types.luau create mode 100644 .lute/lib/build-system/writeBuildCacheAsync.luau create mode 100644 .lute/lib/copyInto.luau create mode 100644 .lute/lib/createRojoProjectAsync.luau create mode 100644 .lute/lib/find.luau create mode 100644 .lute/lib/findWhere.luau create mode 100644 .lute/lib/getStudioPluginsPath.luau create mode 100644 plugin/.darklua.json create mode 100644 project.luau diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..918b5db --- /dev/null +++ b/.env.template @@ -0,0 +1,21 @@ +# +# Logging +# + +LOG_LEVEL=info +# Set this to `true` to have Studio Activity print log messages to the Output window. +ENABLE_OUTPUT_LOGGING=false + +# +# Backend +# + +BASE_URL=activity.brooke.sh + +# +# Open Cloud +# + +ROBLOX_API_KEY= +ROBLOX_UNIT_TESTING_PLACE_ID=137718969043315 +ROBLOX_UNIT_TESTING_UNIVERSE_ID=10128045586 diff --git a/.luaurc b/.luaurc new file mode 100644 index 0000000..4df5766 --- /dev/null +++ b/.luaurc @@ -0,0 +1,12 @@ +{ + "languageMode": "strict", + "aliases": { + "lune": "~/.lune/.typedefs/0.10.4/", + "lint": "~/.lute/typedefs/1.0.0/lint", + "lute": "~/.lute/typedefs/1.0.0/lute", + "std": "~/.lute/typedefs/1.0.0/std", + "pkg": "./Packages", + "repo": ".", + "scripts": "./.lute", + } +} diff --git a/.lune/.luaurc b/.lune/.luaurc deleted file mode 100644 index 74f88fd..0000000 --- a/.lune/.luaurc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "languageMode": "strict", - "aliases": { - "lune": "~/.lune/.typedefs/0.10.4/" - } -} diff --git a/.lute/.config.luau b/.lute/.config.luau index 938232d..94908b2 100644 --- a/.lute/.config.luau +++ b/.lute/.config.luau @@ -6,7 +6,6 @@ return { aliases = { batteries = "../Packages/lute@v1.0.0/batteries", dotenv = "../Packages/dotenv@v0.1.2/src", - std = "~/.lute/typedefs/1.0.0/std", }, }, } diff --git a/.lute/build.luau b/.lute/build.luau new file mode 100644 index 0000000..56b6d41 --- /dev/null +++ b/.lute/build.luau @@ -0,0 +1,157 @@ +local cli = require("@batteries/cli") +local dotenv = require("@dotenv") +local fs = require("@std/fs") +local path = require("@std/path") +local pp = require("@batteries/pp") +local process = require("@std/process") +local richterm = require("@batteries/richterm") +local toml = require("@batteries/toml") + +local buildSystem = require("@scripts/lib/build-system") +local copyInto = require("@scripts/lib/copyInto") +local createRojoProjectAsync = require("@scripts/lib/createRojoProjectAsync") +local getStudioPluginsPath = require("@scripts/lib/getStudioPluginsPath") +local project = require("@repo/project") +-- local dotenv = require("@scripts/lib/dotenv") +local run = require("@scripts/lib/run") +-- local watch = require("@scripts/lib/watch") + +local WALLY_MANIFEST_PATH = path.resolve("plugin/wally.toml") + +type BuildContext = buildSystem.BuildContext + +pcall(dotenv.config) + +local args = cli.parser() + +args:add("channel", "option", { + help = "Channel to build for", + aliases = { "c" }, + default = "prod", +}) +args:add("target", "option", { + help = "Target to build for", + aliases = { "t" }, + default = "local", +}) +args:add("output", "option", { + help = "Full path to the rbxm file to build", + aliases = { "o" }, + default = tostring(path.join(getStudioPluginsPath(), project.PLUGIN_FILENAME)), +}) +args:add("watch", "flag", { + help = "Watch for changes and recompile automatically", + aliases = { "w" }, +}) +args:add("skip-reload", "flag", { + help = "Skip reloading the plugin in Roblox Studio", +}) +args:add("clean", "flag", { + help = "Performs a full rebuild of the project", +}) + +args:parse({ ... }) + +local channel = args:get("channel") +assert(channel == "dev" or channel == "prod", `bad value for channel (must be one of "dev" or "prod", got "{channel}")`) + +local target = args:get("target") +assert( + target == "creator-store" or target == "local", + `bad value for target (must be one of "creator-store" or "local", got "{target}")` +) + +local output = args:get("output") +assert(typeof(output) == "string", `bad value for output (string expected, got {typeof(output)})`) + +local function readWallyManifestAsync() + return toml.deserialize(fs.readFileToString(WALLY_MANIFEST_PATH)) +end + +local function getCommitHash() + local commitHash = run("git", { "rev-parse", "--short", "HEAD" }, { + captureOutput = true, + }) + assert(commitHash ~= nil and commitHash ~= "", "commit hash is empty") + return commitHash +end + +-- This environment variable comes from `.env` and is required to be set. This +-- condition just makes sure if it's _not_ set that the user will then go and +-- get their `.env` file in order +if not process.env.BASE_URL then + error(table.concat({ + "One or more critical environment variables are not set.", + "Please make sure to copy `.env.template` to `.env`. If you already have a `.env` file, make sure the environment variables from the template match your local copy", + }, "\n\n")) +end + +local context: BuildContext = { + channel = channel, + target = target, + shouldRebuild = args:has("clean"), + dest = path.join(project.BUILD_PATH, channel, target), + env = { + BUILD_VERSION = (readWallyManifestAsync() :: any).package.version, + BUILD_CHANNEL = if channel == "prod" then "production" else "development", + BUILD_TARGET = target, + BUILD_HASH = getCommitHash(), + BASE_URL = process.env.BASE_URL, + LOG_LEVEL = process.env.LOG_LEVEL, + }, + cache = buildSystem.readBuildCacheAsync(), +} + +local function buildPluginBinary() + buildSystem.runBuildGroupAsync({ + name = "🔌 plugin binary", + paths = { context.dest }, + context = context, + step = function() + local projectPath = path.resolve(path.join(context.dest, "../default.project.json")) + + createRojoProjectAsync(projectPath, { + name = "StudioActivity", + tree = { + ["$path"] = tostring(path.resolve(context.dest)), + }, + }) + + local dest = path.resolve(path.join(context.dest, "..", project.PLUGIN_FILENAME)) + + run("rojo", { + "build", + projectPath, + "-o", + dest, + }) + + fs.remove(projectPath) + + if args:has("skip-reload") then + if output:match(tostring(getStudioPluginsPath())) then + return + end + end + + copyInto(dest, output) + end, + }) +end + +local function build() + local startTime = os.clock() + + local contextNoCache = table.clone(context) + contextNoCache.cache = nil :: any + print(richterm.dim(`build context: {pp(contextNoCache)}`)) + + buildSystem.compileAsync(context) + buildSystem.writeBuildCacheAsync(context.cache) + + buildPluginBinary() + + print(`build completed in {("%.2f"):format(os.clock() - startTime)}s`) +end + +build() diff --git a/.lute/lib/build-system/compileAsync.luau b/.lute/lib/build-system/compileAsync.luau new file mode 100644 index 0000000..8659037 --- /dev/null +++ b/.lute/lib/build-system/compileAsync.luau @@ -0,0 +1,125 @@ +local fs = require("@std/fs") +local path = require("@std/path") + +local copyInto = require("@scripts/lib/copyInto") +local find = require("@scripts/lib/find") +local project = require("@repo/project") +local run = require("@scripts/lib/run") +local runBuildGroupAsync = require("@scripts/lib/build-system/runBuildGroupAsync") +local types = require("@scripts/lib/build-system/types") + +type BuildContext = types.BuildContext + +local function compileAsync(context: BuildContext) + if context.shouldRebuild then + fs.removeDirectory(context.dest, { + recursive = true, + }) + end + fs.createDirectory(context.dest, { + makeParents = true, + }) + + run("rojo", { + "sourcemap", + project.ROJO_BUILD_PROJECT, + "-o", + project.DARKLUA_SOURCEMAP_PATH, + }) + + -- Darklua is failing to resolve string requires when using absolute paths + -- for the input. Probably related to the sourcemap, though we've tried + -- using the `--absolute` flag with `rojo sourcemap` and that didn't work + -- either. We just massage the source path into a relative path to work + -- around the issue. + local relativeSourcePath = path.relative(project.REPO_PATH, project.SOURCE_PATH) + run("darklua", { + "process", + "--config", + "plugin/.darklua.json", + relativeSourcePath, + context.dest, + }, { + env = context.env, + }) + + runBuildGroupAsync({ + name = "🚚 dependencies", + paths = { + project.PACKAGES_PATH, + project.DEV_PACKAGES_PATH, + }, + context = context, + step = function(group) + for _, filePath in group.paths do + local relativePath = path.relative(project.REPO_PATH, filePath) + copyInto(filePath, path.join(context.dest, relativePath)) + end + end, + }) + + -- for _, member in fs.listDirectory(project.WORKSPACE_PATH) do + -- local memberPath = path.join(project.WORKSPACE_PATH, member.name) + + -- if fs.type(memberPath) == "dir" then + -- compileWorkspaceMemberAsync(memberPath, context) + -- end + -- end + + if context.channel == "prod" then + for _, dir in project.PROD_CONFIG.prunedDirs do + local relativePath = path.relative(project.REPO_PATH, dir) + + fs.removeDirectory(path.join(context.dest, relativePath), { + recursive = true, + }) + end + end + + -- if context.target == "rotriever" then + -- local dest = path.join(project.BUILD_PATH, "flipbook-core-rotriever") + + -- if context.shouldRebuild then + -- fs.removeDirectory(dest, { + -- recursive = true, + -- }) + -- end + + -- copyInto(context.dest, path.join(dest, "src")) + + -- fs.remove(path.join(dest, "src/init.server.luau")) + + -- local manifestPaths = find(dest, "rotriever.toml") + -- if #manifestPaths > 0 then + -- local manifestPath = manifestPaths[1] + -- fs.copy(manifestPath, path.join(dest, "rotriever.toml")) + -- end + + -- -- Bundle up the gigantic dependency bundles into rbxms to alleviate + -- -- path length limit issues that crop up when consuming internally + -- for _, absPackagesPath in { project.PACKAGES_PATH, project.ROBLOX_PACKAGES_PATH } do + -- local relPackagesPath = path.relative(project.REPO_PATH, absPackagesPath) + -- local packagesPath = path.resolve(path.join(dest, "src", relPackagesPath)) + + -- packToRbxm(packagesPath) + + -- fs.removeDirectory(packagesPath, { + -- recursive = true, + -- }) + -- end + + -- local linkerPath = path.join(dest, "src/init.lua") + -- local linkerSource = `return require(script.workspace["flipbook-core"])` + -- fs.writeStringToFile(linkerPath, linkerSource) + + -- -- Rotriever does not support .luau files, so rename + -- -- them to .lua for compatibility + -- for _, luauFile in find(dest, "%.luau$") do + -- local newName = tostring(luauFile):gsub("%.luau$", ".lua") + -- fs.copy(luauFile, newName) + -- fs.remove(luauFile) + -- end + -- end +end + +return compileAsync diff --git a/.lute/lib/build-system/hashPath.luau b/.lute/lib/build-system/hashPath.luau new file mode 100644 index 0000000..a96b3b7 --- /dev/null +++ b/.lute/lib/build-system/hashPath.luau @@ -0,0 +1,48 @@ +local crypto = require("@lute/crypto") +local fs = require("@std/fs") +local path = require("@std/path") + +local HASH_METHOD = crypto.hash.md5 + +local function bufferToHex(buf: buffer): string + local hex = "" + for i = 0, buffer.len(buf) - 1 do + hex ..= ("%02x"):format(buffer.readu8(buf, i)) + end + return hex +end + +local function hashFile(filePath: path.Pathlike): string + local content = fs.readFileToString(filePath) + return bufferToHex(crypto.digest(HASH_METHOD, content)) +end + +local function hashDir(dirPath: path.Pathlike): string + local hashes = {} + + for _, file in fs.listDirectory(dirPath) do + local filePath = path.join(dirPath, file.name) + + if file.type == "dir" then + table.insert(hashes, hashDir(filePath)) + else + table.insert(hashes, hashFile(filePath)) + end + end + + local joinedHash = table.concat(hashes, "") + + return bufferToHex(crypto.digest(HASH_METHOD, joinedHash)) +end + +local function hashPath(filePath: path.Pathlike): string + assert(fs.exists(filePath), `no file found at {filePath}`) + + if fs.type(filePath) == "dir" then + return hashDir(filePath) + else + return hashFile(filePath) + end +end + +return hashPath diff --git a/.lute/lib/build-system/init.luau b/.lute/lib/build-system/init.luau new file mode 100644 index 0000000..19797a6 --- /dev/null +++ b/.lute/lib/build-system/init.luau @@ -0,0 +1,10 @@ +local types = require("@self/types") + +export type BuildContext = types.BuildContext + +return { + compileAsync = require("@self/compileAsync"), + readBuildCacheAsync = require("@self/readBuildCacheAsync"), + writeBuildCacheAsync = require("@self/writeBuildCacheAsync"), + runBuildGroupAsync = require("@self/runBuildGroupAsync"), +} diff --git a/.lute/lib/build-system/readBuildCacheAsync.luau b/.lute/lib/build-system/readBuildCacheAsync.luau new file mode 100644 index 0000000..308cbce --- /dev/null +++ b/.lute/lib/build-system/readBuildCacheAsync.luau @@ -0,0 +1,19 @@ +local fs = require("@std/fs") +local json = require("@std/json") + +local project = require("@repo/project") +local types = require("@scripts/lib/build-system/types") + +local function readBuildCacheAsync(): types.BuildCache + if fs.exists(project.BUILD_CACHE_PATH) then + local content = fs.readFileToString(project.BUILD_CACHE_PATH) + + if content and content ~= "" then + return json.deserialize(content) :: any + end + end + + return {} +end + +return readBuildCacheAsync diff --git a/.lute/lib/build-system/runBuildGroupAsync.luau b/.lute/lib/build-system/runBuildGroupAsync.luau new file mode 100644 index 0000000..fb3cac4 --- /dev/null +++ b/.lute/lib/build-system/runBuildGroupAsync.luau @@ -0,0 +1,57 @@ +local fs = require("@std/fs") +local process = require("@std/process") +local richterm = require("@batteries/richterm") + +local hashPath = require("@scripts/lib/build-system/hashPath") +local types = require("@scripts/lib/build-system/types") + +type BuildGroup = types.BuildGroup + +local function trimDecimals(n: number, decimalPlaces: number) + local factor = math.pow(10, decimalPlaces) + return math.floor(n * factor) / factor +end + +local function runBuildGroupAsync(buildGroup: BuildGroup) + local prefix = richterm.bold(`[{buildGroup.name}]`) + local buildCache = buildGroup.context.cache + local changedPaths = {} + + for _, filePath in buildGroup.paths do + assert(fs.exists(filePath), `attempt to run build group with invalid path (found nothing at {filePath})`) + + local key = `{buildGroup.context.channel}-{buildGroup.context.target}-{filePath}` + local hash = hashPath(filePath) + + if hash ~= buildCache[key] or buildGroup.context.shouldRebuild then + buildCache[key] = hash + table.insert(changedPaths, filePath) + end + end + + if #changedPaths == 0 then + return + end + + local processedBuildGroup = table.clone(buildGroup) + processedBuildGroup.paths = changedPaths + + print(`{prefix} starting build step...`) + + local startTime = os.clock() + local ok, err = xpcall(function() + buildGroup.step(processedBuildGroup) + end, debug.traceback) + + if ok then + local elapsedMs = trimDecimals((os.clock() - startTime) * 1000, 3) + local message = richterm.cyan(`step completed in {elapsedMs}ms`) + print(`{prefix} {message}`) + else + local message = richterm.red(`step failed: {err}`) + print(`{prefix} {message}`) + process.exit(1) + end +end + +return runBuildGroupAsync diff --git a/.lute/lib/build-system/types.luau b/.lute/lib/build-system/types.luau new file mode 100644 index 0000000..17a406b --- /dev/null +++ b/.lute/lib/build-system/types.luau @@ -0,0 +1,27 @@ +local path = require("@std/path") + +export type Channel = "prod" | "dev" +export type Target = "creator-store" | "local" + +export type BuildCache = { + -- Maps file path to MD5 hash + [string]: string, +} + +export type BuildContext = { + channel: Channel, + target: Target, + dest: path.Path, + env: { [string]: string }, + shouldRebuild: boolean, + cache: BuildCache, +} + +export type BuildGroup = { + name: string, + paths: { path.Pathlike }, + context: BuildContext, + step: (self: BuildGroup) -> (), +} + +return nil diff --git a/.lute/lib/build-system/writeBuildCacheAsync.luau b/.lute/lib/build-system/writeBuildCacheAsync.luau new file mode 100644 index 0000000..5848c0d --- /dev/null +++ b/.lute/lib/build-system/writeBuildCacheAsync.luau @@ -0,0 +1,12 @@ +local fs = require("@std/fs") +local json = require("@std/json") + +local project = require("@repo/project") +local types = require("@scripts/lib/build-system/types") + +local function writeBuildCacheAsync(buildCache: types.BuildCache) + local contents = json.serialize(buildCache :: json.Object, true) + fs.writeStringToFile(project.BUILD_CACHE_PATH, contents) +end + +return writeBuildCacheAsync diff --git a/.lute/lib/copyInto.luau b/.lute/lib/copyInto.luau new file mode 100644 index 0000000..1cfec85 --- /dev/null +++ b/.lute/lib/copyInto.luau @@ -0,0 +1,25 @@ +local fs = require("@std/fs") +local path = require("@std/path") + +local function copyInto(src: path.Pathlike, dest: path.Pathlike) + if fs.type(src) == "dir" then + fs.createDirectory(dest, { + makeParents = true, + }) + + for _, file in fs.listDirectory(src) do + local filePath = path.join(src, file.name) + local fileDest = path.join(dest, file.name) + + if file.type == "dir" then + copyInto(filePath, fileDest) + else + fs.copy(filePath, fileDest) + end + end + else + fs.copy(src, dest) + end +end + +return copyInto diff --git a/.lute/lib/createRojoProjectAsync.luau b/.lute/lib/createRojoProjectAsync.luau new file mode 100644 index 0000000..21daee5 --- /dev/null +++ b/.lute/lib/createRojoProjectAsync.luau @@ -0,0 +1,11 @@ +local fs = require("@std/fs") +local json = require("@std/json") +local path = require("@std/path") + +local function createRojoProjectAsync(projectPath: path.Pathlike, projectContent: json.Value): path.Path + local resolvedProjectPath = path.resolve(projectPath) + fs.writeStringToFile(resolvedProjectPath, json.serialize(projectContent, true)) + return resolvedProjectPath +end + +return createRojoProjectAsync diff --git a/.lute/lib/find.luau b/.lute/lib/find.luau new file mode 100644 index 0000000..afbd83c --- /dev/null +++ b/.lute/lib/find.luau @@ -0,0 +1,12 @@ +local path = require("@std/path") + +local findWhere = require("./findWhere") + +local function find(root: path.Pathlike, filenamePattern: string): { path.Path } + return findWhere(root, function(filePath) + local fileName = path.basename(filePath) + return fileName and fileName:match(filenamePattern) ~= nil + end) +end + +return find diff --git a/.lute/lib/findWhere.luau b/.lute/lib/findWhere.luau new file mode 100644 index 0000000..8d2402e --- /dev/null +++ b/.lute/lib/findWhere.luau @@ -0,0 +1,29 @@ +local fs = require("@std/fs") +local path = require("@std/path") + +local function findWhere(rootDirPath: path.Pathlike, matcher: (path: path.Pathlike) -> boolean?): { path.Path } + assert(fs.type(rootDirPath) == "dir", "rootDirPath must be a directory") + + local descendants: { path.Path } = {} + + local function search(rootPath: path.Pathlike) + local isMatch = matcher(rootPath) + if isMatch == nil then + return + elseif isMatch == true then + table.insert(descendants, path.normalize(rootPath)) + end + + if fs.type(rootPath) == "dir" then + for _, child in fs.listDirectory(rootPath) do + search(path.join(rootPath, child.name)) + end + end + end + + search(rootDirPath) + + return descendants +end + +return findWhere diff --git a/.lute/lib/getStudioPluginsPath.luau b/.lute/lib/getStudioPluginsPath.luau new file mode 100644 index 0000000..8dcb99c --- /dev/null +++ b/.lute/lib/getStudioPluginsPath.luau @@ -0,0 +1,13 @@ +local path = require("@std/path") +local process = require("@std/process") +local system = require("@std/system") + +local function getStudioPluginPath(): path.Path + if system.os == "Darwin" then + return path.join(process.homedir(), "Documents/Roblox/Plugins") + else + return path.join(process.homedir(), "AppData/Local/Roblox/Plugins") + end +end + +return getStudioPluginPath diff --git a/plugin/.darklua.json b/plugin/.darklua.json new file mode 100644 index 0000000..4c9e64d --- /dev/null +++ b/plugin/.darklua.json @@ -0,0 +1,61 @@ +{ + "process": [ + { + "rule": "convert_require", + "current": { + "name": "luau" + }, + "target": { + "name": "roblox", + "rojo_sourcemap": "./sourcemap-darklua.json", + "indexing_style": "property" + } + }, + { + "rule": "inject_global_value", + "identifier": "BUILD_VERSION", + "env": "BUILD_VERSION" + }, + { + "rule": "inject_global_value", + "identifier": "BUILD_CHANNEL", + "env": "BUILD_CHANNEL" + }, + { + "rule": "inject_global_value", + "identifier": "BUILD_TARGET", + "env": "BUILD_TARGET" + }, + { + "rule": "inject_global_value", + "identifier": "BUILD_HASH", + "env": "BUILD_HASH" + }, + { + "rule": "inject_global_value", + "identifier": "BASE_URL", + "env": "BASE_URL" + }, + { + "rule": "inject_global_value", + "identifier": "LOG_LEVEL", + "env": "LOG_LEVEL" + }, + { + "rule": "inject_global_value", + "identifier": "ENABLE_OUTPUT_LOGGING", + "env": "ENABLE_OUTPUT_LOGGING" + }, + { + "rule": "inject_global_value", + "identifier": "JEST_TEST_PATH_PATTERN", + "env": "JEST_TEST_PATH_PATTERN" + }, + "compute_expression", + "remove_unused_if_branch", + "remove_unused_while", + "filter_after_early_return", + "remove_nil_declaration", + "remove_empty_do" + ] +} diff --git a/project.luau b/project.luau new file mode 100644 index 0000000..f11f5c6 --- /dev/null +++ b/project.luau @@ -0,0 +1,39 @@ +local path = require("@std/path") + +return { + REPO_PATH = path.resolve("."), + SOURCE_PATH = path.resolve("./plugin/src"), + BUILD_PATH = path.resolve("./plugin/build"), + BUILD_CACHE_PATH = path.resolve("./plugin/build/build-cache.json"), + PACKAGES_PATH = path.resolve("./plugin/Packages"), + DEV_PACKAGES_PATH = path.resolve("./plugin/DevPackages"), + ROBLOX_PACKAGES_VERSION = "0.715.1.7151119", + SCRIPTS_PATH = path.resolve("./.lute"), + + PLUGIN_FILENAME = "StudioActivity.rbxm", + + SOURCEMAP_PATH = path.resolve("./sourcemap.json"), + DARKLUA_SOURCEMAP_PATH = path.resolve("./sourcemap-darklua.json"), + + ROJO_BUILD_PROJECT = path.resolve("./plugin/build.project.json"), + ROJO_TESTS_PROJECT = path.resolve("./plugin/tests.project.json"), + + FOLDERS_TO_LINT = { + ".lune", + ".lute", + "plugin/bin", + "plugin/src", + }, + + PROD_CONFIG = { + prunedDirs = { + path.resolve("./plugin/DevPackages"), + }, + prunedFiles = { + "*.test.lua*", + "*.story.lua*", + "*.storybook.lua*", + "jest.config.lua*", + }, + }, +} diff --git a/rokit.toml b/rokit.toml index 68b4f32..28f7d33 100644 --- a/rokit.toml +++ b/rokit.toml @@ -9,3 +9,4 @@ lune = "lune-org/lune@0.10.4" roblox-packages = "flipbook-labs/roblox-packages@0.5.0" lute = "luau-lang/lute@1.0.0" rocale = "Roblox/rocale-cli@0.1.3" +darklua = "seaofvoices/darklua@0.18.0" From cef8824fdca5fb17335e1a2e73eeb24f8ea524c6 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Wed, 6 May 2026 18:48:16 +0100 Subject: [PATCH 2/8] working build script --- .env.template | 8 +- .lute/build.luau | 63 +++++++++----- .lute/lib/build-system/compileAsync.luau | 85 ++----------------- .lute/lib/build-system/types.luau | 5 +- .../lib/build-system/writeBuildVarsAsync.luau | 15 ++++ .lute/lib/createRojoProjectAsync.luau | 11 --- .lute/lib/find.luau | 12 --- .lute/lib/findWhere.luau | 29 ------- .lute/lib/getDescendants.luau | 26 ++++++ .lute/lib/watch.luau | 71 ++++++++++++++++ .lute/test.luau | 16 ++-- plugin/.darklua.json | 61 ------------- plugin/.gitignore | 2 + plugin/default.project.json | 4 +- plugin/release.project.json | 19 ----- plugin/src/BuildVars/init.luau | 2 +- project.luau | 9 +- rokit.toml | 1 - 18 files changed, 190 insertions(+), 249 deletions(-) create mode 100644 .lute/lib/build-system/writeBuildVarsAsync.luau delete mode 100644 .lute/lib/createRojoProjectAsync.luau delete mode 100644 .lute/lib/find.luau delete mode 100644 .lute/lib/findWhere.luau create mode 100644 .lute/lib/getDescendants.luau create mode 100644 .lute/lib/watch.luau delete mode 100644 plugin/.darklua.json delete mode 100644 plugin/release.project.json diff --git a/.env.template b/.env.template index 918b5db..df44ddb 100644 --- a/.env.template +++ b/.env.template @@ -10,7 +10,13 @@ ENABLE_OUTPUT_LOGGING=false # Backend # -BASE_URL=activity.brooke.sh +API_HOST=activity.brooke.sh + +# +# Discord +# + +DISCORD_CLIENT_ID= # # Open Cloud diff --git a/.lute/build.luau b/.lute/build.luau index 56b6d41..bb04fa3 100644 --- a/.lute/build.luau +++ b/.lute/build.luau @@ -9,12 +9,10 @@ local toml = require("@batteries/toml") local buildSystem = require("@scripts/lib/build-system") local copyInto = require("@scripts/lib/copyInto") -local createRojoProjectAsync = require("@scripts/lib/createRojoProjectAsync") local getStudioPluginsPath = require("@scripts/lib/getStudioPluginsPath") local project = require("@repo/project") --- local dotenv = require("@scripts/lib/dotenv") local run = require("@scripts/lib/run") --- local watch = require("@scripts/lib/watch") +local watch = require("@scripts/lib/watch") local WALLY_MANIFEST_PATH = path.resolve("plugin/wally.toml") @@ -79,7 +77,7 @@ end -- This environment variable comes from `.env` and is required to be set. This -- condition just makes sure if it's _not_ set that the user will then go and -- get their `.env` file in order -if not process.env.BASE_URL then +if not process.env.API_HOST then error(table.concat({ "One or more critical environment variables are not set.", "Please make sure to copy `.env.template` to `.env`. If you already have a `.env` file, make sure the environment variables from the template match your local copy", @@ -91,13 +89,22 @@ local context: BuildContext = { target = target, shouldRebuild = args:has("clean"), dest = path.join(project.BUILD_PATH, channel, target), - env = { - BUILD_VERSION = (readWallyManifestAsync() :: any).package.version, - BUILD_CHANNEL = if channel == "prod" then "production" else "development", - BUILD_TARGET = target, - BUILD_HASH = getCommitHash(), - BASE_URL = process.env.BASE_URL, - LOG_LEVEL = process.env.LOG_LEVEL, + vars = { + build = { + version = (readWallyManifestAsync() :: any).package.version, + channel = channel, + isDev = channel ~= "prod", + hash = getCommitHash(), + }, + + api = { + secure = channel == "prod", + host = process.env.API_HOST, + }, + + discord = { + clientId = process.env.DISCORD_CLIENT_ID, + }, }, cache = buildSystem.readBuildCacheAsync(), } @@ -108,14 +115,8 @@ local function buildPluginBinary() paths = { context.dest }, context = context, step = function() - local projectPath = path.resolve(path.join(context.dest, "../default.project.json")) - - createRojoProjectAsync(projectPath, { - name = "StudioActivity", - tree = { - ["$path"] = tostring(path.resolve(context.dest)), - }, - }) + local projectPath = path.resolve(path.join(context.dest, "default.project.json")) + copyInto(project.ROJO_BUILD_PROJECT, projectPath) local dest = path.resolve(path.join(context.dest, "..", project.PLUGIN_FILENAME)) @@ -155,3 +156,27 @@ local function build() end build() + +if args:has("watch") then + local function onChanged(filePath: path.Path) + print("change?", filePath) + if tostring(filePath):match(tostring(project.PLUGIN_PATH)) then + buildSystem.writeBuildCacheAsync(context.cache) + buildPluginBinary() + else + build() + end + end + + watch({ + roots = { + "./plugin", + "./protos", + }, + excludedFilePatterns = { + ".*Packages", + "build", + }, + onChanged = onChanged, + }) +end diff --git a/.lute/lib/build-system/compileAsync.luau b/.lute/lib/build-system/compileAsync.luau index 8659037..89e8977 100644 --- a/.lute/lib/build-system/compileAsync.luau +++ b/.lute/lib/build-system/compileAsync.luau @@ -2,11 +2,10 @@ local fs = require("@std/fs") local path = require("@std/path") local copyInto = require("@scripts/lib/copyInto") -local find = require("@scripts/lib/find") local project = require("@repo/project") -local run = require("@scripts/lib/run") local runBuildGroupAsync = require("@scripts/lib/build-system/runBuildGroupAsync") local types = require("@scripts/lib/build-system/types") +local writeBuildVarsAsync = require("@scripts/lib/build-system/writeBuildVarsAsync") type BuildContext = types.BuildContext @@ -20,28 +19,11 @@ local function compileAsync(context: BuildContext) makeParents = true, }) - run("rojo", { - "sourcemap", - project.ROJO_BUILD_PROJECT, - "-o", - project.DARKLUA_SOURCEMAP_PATH, - }) + copyInto("plugin/bin", path.join(context.dest, "bin")) + copyInto("plugin/src", path.join(context.dest, "src")) + copyInto("plugin/resources", path.join(context.dest, "resources")) - -- Darklua is failing to resolve string requires when using absolute paths - -- for the input. Probably related to the sourcemap, though we've tried - -- using the `--absolute` flag with `rojo sourcemap` and that didn't work - -- either. We just massage the source path into a relative path to work - -- around the issue. - local relativeSourcePath = path.relative(project.REPO_PATH, project.SOURCE_PATH) - run("darklua", { - "process", - "--config", - "plugin/.darklua.json", - relativeSourcePath, - context.dest, - }, { - env = context.env, - }) + writeBuildVarsAsync(context) runBuildGroupAsync({ name = "🚚 dependencies", @@ -52,74 +34,21 @@ local function compileAsync(context: BuildContext) context = context, step = function(group) for _, filePath in group.paths do - local relativePath = path.relative(project.REPO_PATH, filePath) + local relativePath = path.relative(project.PLUGIN_PATH, filePath) copyInto(filePath, path.join(context.dest, relativePath)) end end, }) - -- for _, member in fs.listDirectory(project.WORKSPACE_PATH) do - -- local memberPath = path.join(project.WORKSPACE_PATH, member.name) - - -- if fs.type(memberPath) == "dir" then - -- compileWorkspaceMemberAsync(memberPath, context) - -- end - -- end - if context.channel == "prod" then for _, dir in project.PROD_CONFIG.prunedDirs do - local relativePath = path.relative(project.REPO_PATH, dir) + local relativePath = path.relative(project.PLUGIN_PATH, dir) fs.removeDirectory(path.join(context.dest, relativePath), { recursive = true, }) end end - - -- if context.target == "rotriever" then - -- local dest = path.join(project.BUILD_PATH, "flipbook-core-rotriever") - - -- if context.shouldRebuild then - -- fs.removeDirectory(dest, { - -- recursive = true, - -- }) - -- end - - -- copyInto(context.dest, path.join(dest, "src")) - - -- fs.remove(path.join(dest, "src/init.server.luau")) - - -- local manifestPaths = find(dest, "rotriever.toml") - -- if #manifestPaths > 0 then - -- local manifestPath = manifestPaths[1] - -- fs.copy(manifestPath, path.join(dest, "rotriever.toml")) - -- end - - -- -- Bundle up the gigantic dependency bundles into rbxms to alleviate - -- -- path length limit issues that crop up when consuming internally - -- for _, absPackagesPath in { project.PACKAGES_PATH, project.ROBLOX_PACKAGES_PATH } do - -- local relPackagesPath = path.relative(project.REPO_PATH, absPackagesPath) - -- local packagesPath = path.resolve(path.join(dest, "src", relPackagesPath)) - - -- packToRbxm(packagesPath) - - -- fs.removeDirectory(packagesPath, { - -- recursive = true, - -- }) - -- end - - -- local linkerPath = path.join(dest, "src/init.lua") - -- local linkerSource = `return require(script.workspace["flipbook-core"])` - -- fs.writeStringToFile(linkerPath, linkerSource) - - -- -- Rotriever does not support .luau files, so rename - -- -- them to .lua for compatibility - -- for _, luauFile in find(dest, "%.luau$") do - -- local newName = tostring(luauFile):gsub("%.luau$", ".lua") - -- fs.copy(luauFile, newName) - -- fs.remove(luauFile) - -- end - -- end end return compileAsync diff --git a/.lute/lib/build-system/types.luau b/.lute/lib/build-system/types.luau index 17a406b..8f783f6 100644 --- a/.lute/lib/build-system/types.luau +++ b/.lute/lib/build-system/types.luau @@ -1,5 +1,8 @@ local path = require("@std/path") +local BuildVars = require("@repo/plugin/src/BuildVars/Types") +type BuildVars = BuildVars.BuildVars + export type Channel = "prod" | "dev" export type Target = "creator-store" | "local" @@ -12,7 +15,7 @@ export type BuildContext = { channel: Channel, target: Target, dest: path.Path, - env: { [string]: string }, + vars: BuildVars, shouldRebuild: boolean, cache: BuildCache, } diff --git a/.lute/lib/build-system/writeBuildVarsAsync.luau b/.lute/lib/build-system/writeBuildVarsAsync.luau new file mode 100644 index 0000000..ffbc87b --- /dev/null +++ b/.lute/lib/build-system/writeBuildVarsAsync.luau @@ -0,0 +1,15 @@ +local fs = require("@std/fs") +local json = require("@std/json") +local path = require("@std/path") + +local project = require("@repo/project") +local types = require("@scripts/lib/build-system/types") + +local function writeBuildVarsAsync(context: types.BuildContext) + local contents = json.serialize(context.vars :: json.Object, true) + + local relativePath = path.relative(project.PLUGIN_PATH, project.BUILD_VARS_PATH) + fs.writeStringToFile(path.join(context.dest, relativePath), contents) +end + +return writeBuildVarsAsync diff --git a/.lute/lib/createRojoProjectAsync.luau b/.lute/lib/createRojoProjectAsync.luau deleted file mode 100644 index 21daee5..0000000 --- a/.lute/lib/createRojoProjectAsync.luau +++ /dev/null @@ -1,11 +0,0 @@ -local fs = require("@std/fs") -local json = require("@std/json") -local path = require("@std/path") - -local function createRojoProjectAsync(projectPath: path.Pathlike, projectContent: json.Value): path.Path - local resolvedProjectPath = path.resolve(projectPath) - fs.writeStringToFile(resolvedProjectPath, json.serialize(projectContent, true)) - return resolvedProjectPath -end - -return createRojoProjectAsync diff --git a/.lute/lib/find.luau b/.lute/lib/find.luau deleted file mode 100644 index afbd83c..0000000 --- a/.lute/lib/find.luau +++ /dev/null @@ -1,12 +0,0 @@ -local path = require("@std/path") - -local findWhere = require("./findWhere") - -local function find(root: path.Pathlike, filenamePattern: string): { path.Path } - return findWhere(root, function(filePath) - local fileName = path.basename(filePath) - return fileName and fileName:match(filenamePattern) ~= nil - end) -end - -return find diff --git a/.lute/lib/findWhere.luau b/.lute/lib/findWhere.luau deleted file mode 100644 index 8d2402e..0000000 --- a/.lute/lib/findWhere.luau +++ /dev/null @@ -1,29 +0,0 @@ -local fs = require("@std/fs") -local path = require("@std/path") - -local function findWhere(rootDirPath: path.Pathlike, matcher: (path: path.Pathlike) -> boolean?): { path.Path } - assert(fs.type(rootDirPath) == "dir", "rootDirPath must be a directory") - - local descendants: { path.Path } = {} - - local function search(rootPath: path.Pathlike) - local isMatch = matcher(rootPath) - if isMatch == nil then - return - elseif isMatch == true then - table.insert(descendants, path.normalize(rootPath)) - end - - if fs.type(rootPath) == "dir" then - for _, child in fs.listDirectory(rootPath) do - search(path.join(rootPath, child.name)) - end - end - end - - search(rootDirPath) - - return descendants -end - -return findWhere diff --git a/.lute/lib/getDescendants.luau b/.lute/lib/getDescendants.luau new file mode 100644 index 0000000..f910e32 --- /dev/null +++ b/.lute/lib/getDescendants.luau @@ -0,0 +1,26 @@ +local fs = require("@std/fs") +local path = require("@std/path") + +local function reduce(rootPath: path.Path, accumulator: { path.Path }): { path.Path } + for _, child in fs.listDirectory(rootPath) do + local childPath = path.join(rootPath, child.name) + table.insert(accumulator, childPath) + + if child.type == "dir" then + reduce(childPath, accumulator) + end + end + return accumulator +end + +local function getDescendants(dirPath: path.Pathlike): { path.Path } + local rootPath = path.resolve(dirPath) + + if fs.type(rootPath) ~= "dir" then + return {} + end + + return reduce(rootPath, {}) +end + +return getDescendants diff --git a/.lute/lib/watch.luau b/.lute/lib/watch.luau new file mode 100644 index 0000000..723ba36 --- /dev/null +++ b/.lute/lib/watch.luau @@ -0,0 +1,71 @@ +local fs = require("@std/fs") +local path = require("@std/path") +local richterm = require("@batteries/richterm") +local task = require("@lute/task") + +local getDescendants = require("./getDescendants") + +type Options = { + roots: { path.Pathlike }, + excludedFilePatterns: { string }?, + onChanged: ((filePath: path.Path) -> ())?, +} + +local function getWatchedFolders(options: Options) + local watchedFolders = {} + + for _, root in options.roots do + for _, descendantPath in getDescendants(root) do + if fs.type(descendantPath) == "dir" then + local isIncluded = true + + if options.excludedFilePatterns then + for _, excludedFilePattern in options.excludedFilePatterns do + if tostring(descendantPath):match(excludedFilePattern) then + isIncluded = false + break + end + end + end + + if isIncluded and tostring(descendantPath):match(tostring(root)) then + table.insert(watchedFolders, descendantPath) + end + end + end + end + + return watchedFolders +end + +local function watch(options: Options) + local watchers = {} + local watcherToFolderMap = {} + + local watchedFolders = getWatchedFolders(options) + + print(richterm.dim("watching folders:")) + for _, watchedFolder in watchedFolders do + print(richterm.dim(`> {watchedFolder}`)) + + local watcher = fs.watch(watchedFolder) + + watcherToFolderMap[watcher] = watchedFolder + table.insert(watchers, watcher) + end + print("listening for file changes...") + + while true do + for _, watcher in watchers do + local event = watcher:next() + local watchedFolder = watcherToFolderMap[watcher] + + if event and event.change and options.onChanged then + options.onChanged(watchedFolder) + end + end + task.wait(0.01) + end +end + +return watch diff --git a/.lute/test.luau b/.lute/test.luau index 2c0a19b..e440f5e 100644 --- a/.lute/test.luau +++ b/.lute/test.luau @@ -1,15 +1,13 @@ local cli = require("@batteries/cli") +local dotenv = require("@dotenv") local path = require("@std/path") local process = require("@std/process") -local run = require("./lib/run") local system = require("@std/system") -local dotenv = require("@dotenv") -pcall(dotenv.config) - -local UNIVERSE_ID = 10128045586 -local PLACE_ID = 137718969043315 +local project = require("@repo/project") +local run = require("./lib/run") +pcall(dotenv.config) local args = cli.parser() args:add("apiKey", "option", { @@ -36,11 +34,11 @@ run("rocale", { "--script", ".lute/tasks/run-tests.luau", "--load.project", - "plugin/tests.project.json", + tostring(project.ROJO_TESTS_PROJECT), "--universeId", - UNIVERSE_ID, + process.env.ROBLOX_UNIT_TESTING_UNIVERSE_ID, "--placeId", - PLACE_ID, + process.env.ROBLOX_UNIT_TESTING_PLACE_ID, "--output", tostring(path.join(system.tmpdir(), "tests.rbxl")), }, { diff --git a/plugin/.darklua.json b/plugin/.darklua.json deleted file mode 100644 index 4c9e64d..0000000 --- a/plugin/.darklua.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "process": [ - { - "rule": "convert_require", - "current": { - "name": "luau" - }, - "target": { - "name": "roblox", - "rojo_sourcemap": "./sourcemap-darklua.json", - "indexing_style": "property" - } - }, - { - "rule": "inject_global_value", - "identifier": "BUILD_VERSION", - "env": "BUILD_VERSION" - }, - { - "rule": "inject_global_value", - "identifier": "BUILD_CHANNEL", - "env": "BUILD_CHANNEL" - }, - { - "rule": "inject_global_value", - "identifier": "BUILD_TARGET", - "env": "BUILD_TARGET" - }, - { - "rule": "inject_global_value", - "identifier": "BUILD_HASH", - "env": "BUILD_HASH" - }, - { - "rule": "inject_global_value", - "identifier": "BASE_URL", - "env": "BASE_URL" - }, - { - "rule": "inject_global_value", - "identifier": "LOG_LEVEL", - "env": "LOG_LEVEL" - }, - { - "rule": "inject_global_value", - "identifier": "ENABLE_OUTPUT_LOGGING", - "env": "ENABLE_OUTPUT_LOGGING" - }, - { - "rule": "inject_global_value", - "identifier": "JEST_TEST_PATH_PATTERN", - "env": "JEST_TEST_PATH_PATTERN" - }, - "compute_expression", - "remove_unused_if_branch", - "remove_unused_while", - "filter_after_early_return", - "remove_nil_declaration", - "remove_empty_do" - ] -} diff --git a/plugin/.gitignore b/plugin/.gitignore index f3d75cf..32e4281 100644 --- a/plugin/.gitignore +++ b/plugin/.gitignore @@ -1,3 +1,5 @@ +build/ + Packages/ ServerPackages/ DevPackages/ diff --git a/plugin/default.project.json b/plugin/default.project.json index c126851..9c6e03d 100644 --- a/plugin/default.project.json +++ b/plugin/default.project.json @@ -15,7 +15,9 @@ "Packages": { "$path": "Packages/", "Dev": { - "$path": "DevPackages/" + "$path": { + "optional": "DevPackages/" + } } } } diff --git a/plugin/release.project.json b/plugin/release.project.json deleted file mode 100644 index 0a409f7..0000000 --- a/plugin/release.project.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "StudioActivity", - "emitLegacyScripts": false, - "tree": { - "$className": "Folder", - "Bin": { - "$path": "bin/" - }, - "Source": { - "$path": "src/" - }, - "Resources": { - "$path": "resources/" - }, - "Packages": { - "$path": "Packages/" - } - } -} diff --git a/plugin/src/BuildVars/init.luau b/plugin/src/BuildVars/init.luau index d7f40cb..5baef0d 100644 --- a/plugin/src/BuildVars/init.luau +++ b/plugin/src/BuildVars/init.luau @@ -1,5 +1,5 @@ local Types = require(script.Types) export type BuildVars = Types.BuildVars -local CompiledBuildVars: BuildVars = require(script.BuildVars) +local CompiledBuildVars: BuildVars = require(script.BuildVars) :: any return table.freeze(CompiledBuildVars) diff --git a/project.luau b/project.luau index f11f5c6..bbb62ab 100644 --- a/project.luau +++ b/project.luau @@ -2,20 +2,17 @@ local path = require("@std/path") return { REPO_PATH = path.resolve("."), - SOURCE_PATH = path.resolve("./plugin/src"), + PLUGIN_PATH = path.resolve("./plugin"), BUILD_PATH = path.resolve("./plugin/build"), BUILD_CACHE_PATH = path.resolve("./plugin/build/build-cache.json"), + BUILD_VARS_PATH = path.resolve("./plugin/src/BuildVars/BuildVars.json"), PACKAGES_PATH = path.resolve("./plugin/Packages"), DEV_PACKAGES_PATH = path.resolve("./plugin/DevPackages"), ROBLOX_PACKAGES_VERSION = "0.715.1.7151119", - SCRIPTS_PATH = path.resolve("./.lute"), PLUGIN_FILENAME = "StudioActivity.rbxm", - SOURCEMAP_PATH = path.resolve("./sourcemap.json"), - DARKLUA_SOURCEMAP_PATH = path.resolve("./sourcemap-darklua.json"), - - ROJO_BUILD_PROJECT = path.resolve("./plugin/build.project.json"), + ROJO_BUILD_PROJECT = path.resolve("./plugin/default.project.json"), ROJO_TESTS_PROJECT = path.resolve("./plugin/tests.project.json"), FOLDERS_TO_LINT = { diff --git a/rokit.toml b/rokit.toml index 28f7d33..68b4f32 100644 --- a/rokit.toml +++ b/rokit.toml @@ -9,4 +9,3 @@ lune = "lune-org/lune@0.10.4" roblox-packages = "flipbook-labs/roblox-packages@0.5.0" lute = "luau-lang/lute@1.0.0" rocale = "Roblox/rocale-cli@0.1.3" -darklua = "seaofvoices/darklua@0.18.0" From 382d31583f9f1b8d92d677ac8795b58632cd6d4f Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Wed, 6 May 2026 18:54:52 +0100 Subject: [PATCH 3/8] prune files --- .lute/lib/build-system/compileAsync.luau | 12 ++++++++++++ project.luau | 7 ------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.lute/lib/build-system/compileAsync.luau b/.lute/lib/build-system/compileAsync.luau index 89e8977..6a69a67 100644 --- a/.lute/lib/build-system/compileAsync.luau +++ b/.lute/lib/build-system/compileAsync.luau @@ -3,6 +3,7 @@ local path = require("@std/path") local copyInto = require("@scripts/lib/copyInto") local project = require("@repo/project") +local run = require("@scripts/lib/run") local runBuildGroupAsync = require("@scripts/lib/build-system/runBuildGroupAsync") local types = require("@scripts/lib/build-system/types") local writeBuildVarsAsync = require("@scripts/lib/build-system/writeBuildVarsAsync") @@ -41,6 +42,17 @@ local function compileAsync(context: BuildContext) }) if context.channel == "prod" then + runBuildGroupAsync({ + name = `✂️ prune build`, + paths = { context.dest }, + context = context, + step = function() + for _, pattern in project.PROD_CONFIG.prunedFiles do + run("find", { context.dest, "-type", "f", "-name", `"{pattern}"`, "-delete" }) + end + end, + }) + for _, dir in project.PROD_CONFIG.prunedDirs do local relativePath = path.relative(project.PLUGIN_PATH, dir) diff --git a/project.luau b/project.luau index bbb62ab..9f4ab00 100644 --- a/project.luau +++ b/project.luau @@ -15,13 +15,6 @@ return { ROJO_BUILD_PROJECT = path.resolve("./plugin/default.project.json"), ROJO_TESTS_PROJECT = path.resolve("./plugin/tests.project.json"), - FOLDERS_TO_LINT = { - ".lune", - ".lute", - "plugin/bin", - "plugin/src", - }, - PROD_CONFIG = { prunedDirs = { path.resolve("./plugin/DevPackages"), From b9965a7806d6e55bc06b7e89b3c15dcd9315353c Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Wed, 6 May 2026 18:55:55 +0100 Subject: [PATCH 4/8] include target in build vars --- .lune/utils/inject-build-vars.luau | 1 + .lute/build.luau | 1 + plugin/src/BuildVars/Types.luau | 1 + 3 files changed, 3 insertions(+) diff --git a/.lune/utils/inject-build-vars.luau b/.lune/utils/inject-build-vars.luau index da04d30..3a89902 100644 --- a/.lune/utils/inject-build-vars.luau +++ b/.lune/utils/inject-build-vars.luau @@ -34,6 +34,7 @@ local function injectBuildVars(config: Config) build = { version = version, channel = "live", + target = "", hash = gitHash, isDev = not isProd, }, diff --git a/.lute/build.luau b/.lute/build.luau index bb04fa3..819a9f2 100644 --- a/.lute/build.luau +++ b/.lute/build.luau @@ -93,6 +93,7 @@ local context: BuildContext = { build = { version = (readWallyManifestAsync() :: any).package.version, channel = channel, + target = target, isDev = channel ~= "prod", hash = getCommitHash(), }, diff --git a/plugin/src/BuildVars/Types.luau b/plugin/src/BuildVars/Types.luau index 59d2d6f..fb91ba6 100644 --- a/plugin/src/BuildVars/Types.luau +++ b/plugin/src/BuildVars/Types.luau @@ -2,6 +2,7 @@ export type BuildVars = { build: { version: string, channel: string, + target: string, hash: string, isDev: boolean, }, From 0e8ef1e5bbb43d27070a301f67931430ed0f0604 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Sat, 9 May 2026 21:22:40 +0100 Subject: [PATCH 5/8] move over lint and analyze commands --- .lute/analyze.luau | 53 +++++++++++++++++++ .../lib/build-system/writeBuildVarsAsync.luau | 2 +- .lute/lib/find.luau | 12 +++++ .lute/lib/findWhere.luau | 29 ++++++++++ .lute/lint.luau | 36 +++++++++++++ .lute/test.luau | 5 ++ .vscode/settings.json | 16 +++--- plugin/bin/TestRunner.luau | 6 ++- plugin/bin/setup.luau | 2 +- project.luau | 9 ++++ 10 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 .lute/analyze.luau create mode 100644 .lute/lib/find.luau create mode 100644 .lute/lib/findWhere.luau create mode 100644 .lute/lint.luau diff --git a/.lute/analyze.luau b/.lute/analyze.luau new file mode 100644 index 0000000..95fb3cd --- /dev/null +++ b/.lute/analyze.luau @@ -0,0 +1,53 @@ +local fs = require("@std/fs") +local path = require("@std/path") + +local project = require("@repo/project") +local run = require("@scripts/lib/run") + +local globalDefsPath = path.resolve("./plugin/generated/globalTypes.d.luau") + +if not fs.exists(globalDefsPath) then + run("curl", { + "-s", + "-o", + globalDefsPath, + "-O", + "https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/master/scripts/globalTypes.d.lua", + }) +end + +run("rojo", { + "sourcemap", + project.ROJO_BUILD_PROJECT, + "-o", + project.SOURCEMAP_PATH, + "--absolute", +}) + +local defaultArgs: { string | path.Pathlike } = { + "analyze", + `--defs={globalDefsPath}`, + "--settings=./.vscode/settings.json", +} + +-- New solver analysis +-- TODO: Work out why Lute doesn't analyze +-- do +-- local args = table.clone(defaultArgs) +-- table.insert(args, "--platform=standard") +-- table.insert(args, "--flag:LuauSolverV2=true") +-- table.insert(args, path.resolve("./.lute")) + +-- run("luau-lsp", { table.unpack(args) }) +-- end + +-- Old solver analysis +do + local args = table.clone(defaultArgs) + table.insert(args, `--sourcemap={project.SOURCEMAP_PATH}`) + table.insert(args, path.resolve("./.lute")) + table.insert(args, path.resolve("./plugin/src")) + table.insert(args, path.resolve("./plugin/bin")) + + run("luau-lsp", { table.unpack(args) }) +end diff --git a/.lute/lib/build-system/writeBuildVarsAsync.luau b/.lute/lib/build-system/writeBuildVarsAsync.luau index ffbc87b..3a4d5aa 100644 --- a/.lute/lib/build-system/writeBuildVarsAsync.luau +++ b/.lute/lib/build-system/writeBuildVarsAsync.luau @@ -6,7 +6,7 @@ local project = require("@repo/project") local types = require("@scripts/lib/build-system/types") local function writeBuildVarsAsync(context: types.BuildContext) - local contents = json.serialize(context.vars :: json.Object, true) + local contents = json.serialize(context.vars :: any, true) local relativePath = path.relative(project.PLUGIN_PATH, project.BUILD_VARS_PATH) fs.writeStringToFile(path.join(context.dest, relativePath), contents) diff --git a/.lute/lib/find.luau b/.lute/lib/find.luau new file mode 100644 index 0000000..afbd83c --- /dev/null +++ b/.lute/lib/find.luau @@ -0,0 +1,12 @@ +local path = require("@std/path") + +local findWhere = require("./findWhere") + +local function find(root: path.Pathlike, filenamePattern: string): { path.Path } + return findWhere(root, function(filePath) + local fileName = path.basename(filePath) + return fileName and fileName:match(filenamePattern) ~= nil + end) +end + +return find diff --git a/.lute/lib/findWhere.luau b/.lute/lib/findWhere.luau new file mode 100644 index 0000000..8d2402e --- /dev/null +++ b/.lute/lib/findWhere.luau @@ -0,0 +1,29 @@ +local fs = require("@std/fs") +local path = require("@std/path") + +local function findWhere(rootDirPath: path.Pathlike, matcher: (path: path.Pathlike) -> boolean?): { path.Path } + assert(fs.type(rootDirPath) == "dir", "rootDirPath must be a directory") + + local descendants: { path.Path } = {} + + local function search(rootPath: path.Pathlike) + local isMatch = matcher(rootPath) + if isMatch == nil then + return + elseif isMatch == true then + table.insert(descendants, path.normalize(rootPath)) + end + + if fs.type(rootPath) == "dir" then + for _, child in fs.listDirectory(rootPath) do + search(path.join(rootPath, child.name)) + end + end + end + + search(rootDirPath) + + return descendants +end + +return findWhere diff --git a/.lute/lint.luau b/.lute/lint.luau new file mode 100644 index 0000000..240291c --- /dev/null +++ b/.lute/lint.luau @@ -0,0 +1,36 @@ +local path = require("@std/path") +local process = require("@std/process") + +local findWhere = require("@scripts/lib/findWhere") +local project = require("@repo/project") +local run = require("@scripts/lib/run") + +local function findLuaFiles() + local result = {} + + for _, folder in project.FOLDERS_TO_LINT do + local matches = findWhere(folder, function(filePath) + return path.extname(filePath) == ".lua" + end) + + for _, match in matches do + table.insert(result, match) + end + end + + return result +end + +run("selene", { table.unpack(project.FOLDERS_TO_LINT) }) + +run("stylua", { + "--check", + table.unpack(project.FOLDERS_TO_LINT), +}) + +local files = findLuaFiles() +if #files > 0 then + print("[err] the following file(s) are using the '.lua' extension. Please change to '.luau' and try again") + print(`{table.concat(files, "\n")}`) + process.exit(1) +end diff --git a/.lute/test.luau b/.lute/test.luau index e440f5e..d7ba253 100644 --- a/.lute/test.luau +++ b/.lute/test.luau @@ -29,6 +29,9 @@ if not apiKey then ) end +local filter = args:get("filter") +local testPathPattern = if filter then filter else "__null__" + run("rocale", { "run", "--script", @@ -41,6 +44,8 @@ run("rocale", { process.env.ROBLOX_UNIT_TESTING_PLACE_ID, "--output", tostring(path.join(system.tmpdir(), "tests.rbxl")), + "--lua.globals", + `JEST_TEST_PATH_PATTERN={testPathPattern}`, }, { env = { ROBLOX_API_KEY = apiKey, diff --git a/.vscode/settings.json b/.vscode/settings.json index 9912136..4dff554 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,9 @@ { - "luau-lsp.completion.imports.enabled": true, - "luau-lsp.completion.imports.suggestServices": true, - "luau-lsp.completion.imports.suggestRequires": true, - "luau-lsp.completion.imports.separateGroupsWithLine": true, - "luau-lsp.sourcemap.useVSCodeWatcher": true, - "luau-lsp.sourcemap.rojoProjectFile": "plugin/generated/sourcemap.project.json", - "luau-lsp.sourcemap.sourcemapFile": "plugin/generated/sourcemap.json", - "luau-lsp.sourcemap.generatorCommand": "lune run codegen", - "editor.formatOnSave": true + "luau-lsp.sourcemap.rojoProjectFile": "plugin/default.project.json", + "luau-lsp.ignoreGlobs": [ + "**/_Index/**", + "**/build/**", + "**/*Packages/**", + "**/.lute/typedefs/**" + ] } diff --git a/plugin/bin/TestRunner.luau b/plugin/bin/TestRunner.luau index 550429a..6b9ddcf 100644 --- a/plugin/bin/TestRunner.luau +++ b/plugin/bin/TestRunner.luau @@ -15,11 +15,13 @@ local function runTests() Charm.flags.strict = true Charm.flags.frozen = true + -- selene: allow(global_usage) + local testPathPattern = _G.JEST_TEST_PATH_PATTERN + local status, result = Jest.runCLI(root, { verbose = false, ci = false, - -- selene: allow(global_usage) - testPathPattern = _G.JEST_TEST_PATH_PATTERN, + testPathPattern = if testPathPattern ~= "__null__" then testPathPattern else nil, }, { root }):awaitStatus() if status == "Rejected" then diff --git a/plugin/bin/setup.luau b/plugin/bin/setup.luau index 3e94439..223bcbe 100644 --- a/plugin/bin/setup.luau +++ b/plugin/bin/setup.luau @@ -105,7 +105,7 @@ local function setup(plugin: Plugin, main: PluginMain) 200 --minSize.Y ), getDockTitle = function(localize): string - return localize("Plugin", "Name") + return localize("Plugin", "Name") .. if BuildVars.build.isDev then ` [{BuildVars.build.hash}]` else "" end, zIndexBehavior = Enum.ZIndexBehavior.Sibling, }, diff --git a/project.luau b/project.luau index 9f4ab00..237df16 100644 --- a/project.luau +++ b/project.luau @@ -12,9 +12,18 @@ return { PLUGIN_FILENAME = "StudioActivity.rbxm", + SOURCEMAP_PATH = path.resolve("./plugin/generated/sourcemap.json"), + ROJO_BUILD_PROJECT = path.resolve("./plugin/default.project.json"), ROJO_TESTS_PROJECT = path.resolve("./plugin/tests.project.json"), + FOLDERS_TO_LINT = { + ".lune", + ".lute", + "plugin/bin", + "plugin/src", + }, + PROD_CONFIG = { prunedDirs = { path.resolve("./plugin/DevPackages"), From bbc7f9ed13feb5afdbcdd062fb657e9469dd06c6 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Mon, 18 May 2026 22:10:01 -0400 Subject: [PATCH 6/8] fix parsing --- .luaurc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.luaurc b/.luaurc index 4df5766..d1f9489 100644 --- a/.luaurc +++ b/.luaurc @@ -7,6 +7,6 @@ "std": "~/.lute/typedefs/1.0.0/std", "pkg": "./Packages", "repo": ".", - "scripts": "./.lute", + "scripts": "./.lute" } } From b1fb083fff342ff84c5a733d2e1b80ac0e59da25 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Mon, 18 May 2026 22:27:13 -0400 Subject: [PATCH 7/8] fix c --- .github/workflows/analyze.yml | 4 +--- .github/workflows/release.yml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index 7302b0e..513c477 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -26,9 +26,7 @@ jobs: run: rokit list - name: Run Lune setup - run: | - lune setup - rm -f .luaurc + run: lune setup - name: 📦 Setup project run: lune run setup diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32f7614..6a1f392 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,9 +38,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Run Lune setup - run: | - lune setup - rm -f .luaurc + run: lune setup - name: 📦 Setup project run: lune run setup From 88d030d3531b3aa12b047be378619464c049dfec Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Mon, 18 May 2026 22:35:35 -0400 Subject: [PATCH 8/8] fix ci --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6956eb8..ae91712 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,3 +43,5 @@ jobs: run: lute run test env: ROBLOX_API_KEY: ${{ secrets.ROBLOX_API_KEY }} + ROBLOX_UNIT_TESTING_UNIVERSE_ID: "10128045586" + ROBLOX_UNIT_TESTING_PLACE_ID: "137718969043315"