diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..df44ddb --- /dev/null +++ b/.env.template @@ -0,0 +1,27 @@ +# +# Logging +# + +LOG_LEVEL=info +# Set this to `true` to have Studio Activity print log messages to the Output window. +ENABLE_OUTPUT_LOGGING=false + +# +# Backend +# + +API_HOST=activity.brooke.sh + +# +# Discord +# + +DISCORD_CLIENT_ID= + +# +# Open Cloud +# + +ROBLOX_API_KEY= +ROBLOX_UNIT_TESTING_PLACE_ID=137718969043315 +ROBLOX_UNIT_TESTING_UNIVERSE_ID=10128045586 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 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" diff --git a/.luaurc b/.luaurc new file mode 100644 index 0000000..d1f9489 --- /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/.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/.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/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/build.luau b/.lute/build.luau new file mode 100644 index 0000000..819a9f2 --- /dev/null +++ b/.lute/build.luau @@ -0,0 +1,183 @@ +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 getStudioPluginsPath = require("@scripts/lib/getStudioPluginsPath") +local project = require("@repo/project") +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.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", + }, "\n\n")) +end + +local context: BuildContext = { + channel = channel, + target = target, + shouldRebuild = args:has("clean"), + dest = path.join(project.BUILD_PATH, channel, target), + vars = { + build = { + version = (readWallyManifestAsync() :: any).package.version, + channel = channel, + target = target, + isDev = channel ~= "prod", + hash = getCommitHash(), + }, + + api = { + secure = channel == "prod", + host = process.env.API_HOST, + }, + + discord = { + clientId = process.env.DISCORD_CLIENT_ID, + }, + }, + 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")) + copyInto(project.ROJO_BUILD_PROJECT, projectPath) + + 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() + +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 new file mode 100644 index 0000000..6a69a67 --- /dev/null +++ b/.lute/lib/build-system/compileAsync.luau @@ -0,0 +1,66 @@ +local fs = require("@std/fs") +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") + +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, + }) + + copyInto("plugin/bin", path.join(context.dest, "bin")) + copyInto("plugin/src", path.join(context.dest, "src")) + copyInto("plugin/resources", path.join(context.dest, "resources")) + + writeBuildVarsAsync(context) + + 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.PLUGIN_PATH, filePath) + copyInto(filePath, path.join(context.dest, relativePath)) + end + end, + }) + + 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) + + fs.removeDirectory(path.join(context.dest, relativePath), { + recursive = true, + }) + 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..8f783f6 --- /dev/null +++ b/.lute/lib/build-system/types.luau @@ -0,0 +1,30 @@ +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" + +export type BuildCache = { + -- Maps file path to MD5 hash + [string]: string, +} + +export type BuildContext = { + channel: Channel, + target: Target, + dest: path.Path, + vars: BuildVars, + 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/build-system/writeBuildVarsAsync.luau b/.lute/lib/build-system/writeBuildVarsAsync.luau new file mode 100644 index 0000000..3a4d5aa --- /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 :: any, 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/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/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/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/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/.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/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 2c0a19b..d7ba253 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", { @@ -31,18 +29,23 @@ if not apiKey then ) end +local filter = args:get("filter") +local testPathPattern = if filter then filter else "__null__" + run("rocale", { "run", "--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")), + "--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/.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/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 67a2e1d..9561fa0 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/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/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, }, 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 new file mode 100644 index 0000000..237df16 --- /dev/null +++ b/project.luau @@ -0,0 +1,38 @@ +local path = require("@std/path") + +return { + REPO_PATH = path.resolve("."), + 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", + + 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"), + }, + prunedFiles = { + "*.test.lua*", + "*.story.lua*", + "*.storybook.lua*", + "jest.config.lua*", + }, + }, +}