diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e57fe2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Pesde packages +*_packages/ +pesde.lock + +# Generated scripts +.pesde/* +!.pesde/.gitkeep diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d382b7a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test-files/rojo"] + path = test-files/rojo + url = https://github.com/rojo-rbx/rojo.git diff --git a/.luaurc b/.luaurc new file mode 100644 index 0000000..2cc120f --- /dev/null +++ b/.luaurc @@ -0,0 +1,7 @@ +{ + "languageMode": "nonstrict", + "lint": { + "*": true, + "TableOperations": false + } +} \ No newline at end of file diff --git a/.lune/build.luau b/.lune/build.luau new file mode 100644 index 0000000..a3577cd --- /dev/null +++ b/.lune/build.luau @@ -0,0 +1,225 @@ +--> Generates sync config and sourcemap scripts for supported tools + +local process = require("@lune/process") +local serde = require("@lune/serde") +local stdio = require("@lune/stdio") +local task = require("@lune/task") + +local lib = require("../src") +local manifest = require("./lib/manifest") +local pathfs = require("../lune_packages/pathfs") + +type ToolChoice = "rojo" +type ManifestExt = { + scripts: { + [ToolChoice]: { + version: string, + tool_dependencies: { { [string]: manifest.DependencySpecifier } }, + }, + }, +} + +local SCRIPTS_DIR = pathfs.getAbsolutePathOf(pathfs.Path.from(".pesde")) +local MANIFEST = manifest(nil, (nil :: any) :: { meta: ManifestExt }) +local SCRIPTS = { + syncConfigGenerator = [[local process = require("@lune/process") + +local args = table.clone(process.args) +local ok, _ = +require("./lune_packages/core").generators.%s.syncConfig(table.remove(args, 1), args, { writeToFile = true }) +if not ok then + return process.exit(1) +end]], + + sourcemapGenerator = [[local process = require("@lune/process") + +local ok = require("./lune_packages/core").generators.%s.sourcemap(process.args[1]) +if not ok then + return process.exit(1) +end +]], +} + +local README_TMPL = [[# `pesde/scripts_%s` +Common scripts intended for use with %s. + +## Included scripts +### roblox_sync_config_generator + +Generates a %s sync config for pesde Roblox packages. + +### sourcemap_generator + +Prints out a sourcemap for Wally dependencies for type extraction. +]] + +local function logPrefix(type: "error" | "info") + local statusColor: stdio.Color = if type == "error" + then "red" + elseif type == "info" then "green" + else error(`Invalid type: {type}`) + + return `main::{stdio.style("bold")}{stdio.color(statusColor)}{type}{stdio.color( + "reset" + )}` +end + +local INFO_PREFIX = `[ {logPrefix("info")}]` +local _ERROR_PREFIX = `[{logPrefix("error")}]` + +local function installDeps(): number + local PESDE_INFO_PREFIX = + string.gsub(`[{logPrefix("info")}]`, "main", "pesde") + local PESDE_ERROR_PREFIX = + string.gsub(`[{logPrefix("error")}]`, "main", "pesde") + local SPINNER_STATES = + { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" } + + stdio.write(`{PESDE_INFO_PREFIX} Installing dependencies with pesde`) + + -- Spawn our thread to display the spinner + local spinnerThread = task.spawn(function() + -- Hide the cursor + stdio.write("\x1b[?25l") + + -- Display the spinner + while true do + for _, state in SPINNER_STATES do + stdio.write(` {state}`) + stdio.write( + -- Moves the cursor back 1 spot and clears everything after + "\x1b[2D\x1b[0K" + ) + end + end + end) + + -- `process_spawn` is an async rust function, so tokio spawns a hardware + -- thread and mlua will yield the current thread, allowing for `spinnerThread` + -- to continue executing + local child = process.spawn("pesde", { "install" }, { + stdio = "default", + }) + + -- If we reach this point, that means mlua resumed the current thread and + -- `process_spawn` returned or errored. We can now close `spinnerThread` and + -- clean up + + task.cancel(spinnerThread) + stdio.write( + -- Clear the current line, move cursor back to its beginning and + -- show it + "\x1b[2K\x1b[1G\x1b[?25h" + ) + + if not child.ok then + stdio.ewrite( + `{PESDE_ERROR_PREFIX} Failed to install dependencies with pesde, error:\n` + ) + stdio.ewrite(child.stderr) + end + + stdio.write( + `{PESDE_INFO_PREFIX} Installed dependencies with pesde successfully\n` + ) + + return child.code +end + +for tool, generators in lib.generators do + local startTime = os.clock() + + -- For each tool, we generate a respective manifests and scripts + local toolChoice = tool :: ToolChoice + local toolScriptsDir = SCRIPTS_DIR:join(toolChoice) + local toolMeta = MANIFEST.meta.scripts[toolChoice] + + if not pathfs.isDir(toolScriptsDir) then + pathfs.writeDir(toolScriptsDir) + end + + local capitalisedToolChoice = string.gsub(toolChoice, "^%l", string.upper) + + -- Define the manifest for the tool + local toolManifest: manifest.PesdeManifest = { + name = `pesde/scripts_{toolChoice}`, + version = toolMeta.version, + -- For the description, we capitalize the first letter of the tool name here + -- since it is a proper noun + description = `Scripts for {capitalisedToolChoice}-based Roblox projects.`, + authors = { + "daimond113 (https://www.daimond113.com/)", + "Erica Marigold ", + }, + repository = "https://github.com/pesde-pkg/scripts", + license = "MIT", + includes = { + "roblox_sync_config_generator.luau", + "sourcemap_generator.luau", + "pesde.toml", + "README.md", + "LICENSE", + }, + + target = { + environment = "lune", + scripts = { + roblox_sync_config_generator = "roblox_sync_config_generator.luau", + sourcemap_generator = "sourcemap_generator.luau", + }, + }, + + peer_dependencies = toolMeta.tool_dependencies, + dependencies = { + core = { workspace = "pesde/scripts_core", version = "^" }, + }, + + indices = { + default = "https://github.com/pesde-pkg/index", + }, + } + + -- Format the scripts for the tool + local syncConfigGeneratorScript, sourcemapGeneratorScript = + string.format(SCRIPTS.syncConfigGenerator, toolChoice), + string.format(SCRIPTS.sourcemapGenerator, toolChoice) + + -- Finally, write all the generated files + pathfs.writeFile( + toolScriptsDir:join("pesde.toml"), + serde.encode("toml", toolManifest, true) + ) + pathfs.writeFile( + toolScriptsDir:join("roblox_sync_config_generator.luau"), + syncConfigGeneratorScript + ) + pathfs.writeFile( + toolScriptsDir:join("sourcemap_generator.luau"), + sourcemapGeneratorScript + ) + pathfs.writeFile( + toolScriptsDir:join("README.md"), + string.format( + README_TMPL, + toolChoice, + capitalisedToolChoice, + capitalisedToolChoice + ) + ) + pathfs.copy( + pathfs.cwd:join("LICENSE"), + toolScriptsDir:join("LICENSE"), + true + ) + + stdio.write( + `{INFO_PREFIX} Generated script project for tool {toolChoice} ({stdio.style( + "dim" + )}{string.format("%.2fms", (os.clock() - startTime) * 1000)}{stdio.style( + "reset" + )})\n` + ) +end + +-- Now we install the dependencies for the newly created projects +process.exit(installDeps()) diff --git a/.lune/fmt.luau b/.lune/fmt.luau new file mode 100644 index 0000000..1976516 --- /dev/null +++ b/.lune/fmt.luau @@ -0,0 +1,13 @@ +--> Run stylua to check for formatting errors + +local process = require("@lune/process") + +local CommandBuilder = require("./lib/exec") + +process.exit( + CommandBuilder.new("stylua") + :withArg(".") + :withArgs(process.args) + :withStdioStrategy("forward") + :exec().code +) diff --git a/.lune/lib/channel.luau b/.lune/lib/channel.luau new file mode 100644 index 0000000..bce40c0 --- /dev/null +++ b/.lune/lib/channel.luau @@ -0,0 +1,48 @@ +--- An MPSC synchronization primitive powered by Lua upvalues which retains only +--- one value at a time. + +--- ## Usage +--- ```luau +--- local send, recv = watch((nil :: any) :: string) +--- task.delay(5, send, "hello, world!") +--- task.spawn(function() +--- local value = recv() +--- print("received value:", value) +--- end) +--- ``` +type Watch = { + value: T?, + receivers: { thread }, +} + +--- Creates a new `Watch` channel, returning its send and receive handles. +local function chan(_phantom: T): ((T) -> (), () -> T?) + local watch: Watch = { + value = nil, + receivers = {}, + } + + local function send(value: T) + watch.value = value + + for _, receiver in watch.receivers do + coroutine.resume(receiver, value) + end + end + + local function recv(): T + local value = watch.value + watch.value = nil + + if value == nil then + table.insert(watch.receivers, coroutine.running()) + return coroutine.yield() + end + + return value :: T + end + + return send, recv +end + +return chan diff --git a/.lune/lib/exec.luau b/.lune/lib/exec.luau new file mode 100644 index 0000000..0bcc6dd --- /dev/null +++ b/.lune/lib/exec.luau @@ -0,0 +1,123 @@ +--> lib: Builder pattern class to spawn child processes + +local process = require("@lune/process") +local stdio = require("@lune/stdio") + +local CommandBuilder = {} + +export type CommandBuilder = typeof(setmetatable( + {} :: CommandBuilderFields, + { __index = CommandBuilder } +)) +type CommandBuilderFields = { + program: string, + args: { string }, + stdioStrategy: IoStrategyMapping?, +} +export type StdioStrategy = "pipe" | "forward" | "none" +export type IoStrategyMapping = { + stdout: StdioStrategy?, + stderr: StdioStrategy?, +} + +local DEFAULT_STDIO_STRATEGY: IoStrategyMapping = { + stdout = "pipe", + stderr = "pipe", +} +function CommandBuilder.new(program: string) + return setmetatable( + { + program = program, + args = {}, + stdioStrategy = nil, + } :: CommandBuilderFields, + { + __index = CommandBuilder, + } + ) +end + +function CommandBuilder.withArg( + self: CommandBuilder, + arg: string +): CommandBuilder + table.insert(self.args, arg) + return self +end + +function CommandBuilder.withArgs( + self: CommandBuilder, + args: { string } +): CommandBuilder + for _, arg in args do + self:withArg(arg) + end + + return self +end + +function CommandBuilder.withStdioStrategy( + self: CommandBuilder, + strategy: StdioStrategy | IoStrategyMapping +): CommandBuilder + self.stdioStrategy = if typeof(strategy) == "string" + then { + stdout = strategy, + stderr = strategy, + } + else strategy + return self +end + +local function intoSpawnOptionsStdioKind( + strategy: StdioStrategy +): process.SpawnOptionsStdioKind + if strategy == "pipe" then + return "default" + end + + if strategy == "forward" then + return "forward" + end + + if strategy == "none" then + return "none" + end + + error(`Non-strategy provided: {strategy}`) +end + +function CommandBuilder.exec(self: CommandBuilder): process.SpawnResult + print( + "$", + stdio.style("dim") .. self.program, + table.concat(self.args, " ") .. stdio.style("reset") + ) + + local function translateIoStrategyMappings(mappings: IoStrategyMapping) + local translatedMappings: process.SpawnOptionsStdio = {} + for field, value in mappings do + translatedMappings[field] = intoSpawnOptionsStdioKind(value) + end + + return translatedMappings + end + + local child = process.spawn(self.program, self.args, { + shell = true, + stdio = translateIoStrategyMappings( + self.stdioStrategy or DEFAULT_STDIO_STRATEGY + ), + }) + + if not child.ok then + print( + `\n{stdio.color("red")}[luau-lsp]{stdio.color("reset")} Exited with code`, + child.code + ) + end + + return child +end + +return CommandBuilder diff --git a/.lune/lib/manifest.luau b/.lune/lib/manifest.luau new file mode 100644 index 0000000..5380bf1 --- /dev/null +++ b/.lune/lib/manifest.luau @@ -0,0 +1,80 @@ +local fs = require("@lune/fs") +local serde = require("@lune/serde") + +export type SPDXLicense = + "MIT" + | "Apache-2.0" + | "BSD-2-Clause" + | "BSD-3-Clause" + | "GPL-2.0" + | "GPL-3.0" + | "LGPL-2.1" + | "LGPL-3.0" + | "MPL-2.0" + | "ISC" + | "Unlicense" + | "WTFPL" + | "Zlib" + | "CC0-1.0" + | "CC-BY-4.0" + | "CC-BY-SA-4.0" + | "BSL-1.0" + | "EPL-2.0" + | "AGPL-3.0" + +export type DependencySpecifier = (({ + name: string, + version: string, + index: string?, +} | { workspace: string, version: string }) & { + target: string?, +}) | { + wally: string, + version: string, + index: string?, +} | { + repo: string, + rev: string, + path: string?, +} + +export type PackageTarget = { + environment: "roblox" | "roblox_server", + lib: string, +} | { + environment: "luau" | "lune", + lib: string, + bin: string, + scripts: { [string]: string }, +} + +export type PesdeManifest = { + name: string, + version: string, + description: string?, + license: SPDXLicense?, + authors: { string }?, + repository: string?, + private: boolean?, + includes: { string }?, + pesde_version: string?, + workspace_members: { string }?, + target: PackageTarget, + build_files: { string }?, + scripts: { [string]: string }?, + indices: { [string]: string }, + wally_indices: { [string]: string }?, + overrides: { [string]: DependencySpecifier }?, + patches: { [string]: { [string]: string } }?, + place: { [string]: string }?, + dependencies: { [string]: DependencySpecifier }?, + peer_dependencies: { [string]: DependencySpecifier }?, + dev_dependencies: { [string]: DependencySpecifier }?, +} & T + +return function(path: string?, _phantom: T): PesdeManifest + local manifestContents = fs.readFile(path or "pesde.toml") + local decoded = serde.decode("toml", manifestContents) + + return decoded :: PesdeManifest +end diff --git a/.lune/tests/init.luau b/.lune/tests/init.luau new file mode 100644 index 0000000..42de320 --- /dev/null +++ b/.lune/tests/init.luau @@ -0,0 +1,81 @@ +--> Run tests using frktest runner + +local fs = require("@lune/fs") +local process = require("@lune/process") + +local frktest = require("../../lune_packages/frktest") +local reporter = require("./reporter") + +-- HACK: Cast require to allow for dynamic paths in strict mode +-- A more proper solution would be to use luau.load instead, but +-- frktest requires its global state to be modified by test suites +local require = require :: ( + path: string +) -> ( + test: typeof(setmetatable( + {} :: { + case: (name: string, fn: () -> nil) -> (), + suite: (name: string, fn: () -> ()) -> (), + }, + { __index = frktest.test } + )) +) -> () + +local function discoverTests(dir: string) + local tests = {} + for _, file in fs.readDir(dir) do + local fullPath = `{dir}/{file}` + + -- Look for files ending in `.spec.luau` as tests + if fs.isFile(fullPath) and string.sub(file, -10) == ".spec.luau" then + table.insert(tests, fullPath) + end + + -- Recurse for directories + if fs.isDir(fullPath) then + local moreTests = discoverTests(fullPath) + + -- Why are the indices starting at 0???? What???? + table.move(moreTests, 0, #moreTests, #tests, tests) + end + end + + return tests +end + +local allowedTests = process.args +for _, test in discoverTests("src") do + -- If we are given any arguments, we only run those tests, otherwise, + -- we run all the tests + + -- So, to include only a certain set of test files, you can provide either + -- the full path of the test file or name of the test file, with or without + -- the `.spec.luau` extension + local baseName = string.match(test, "([^/\\]+)$") + + local withoutExt = string.sub(test, 1, -11) + local baseNameWithoutExt = string.match(withoutExt, "([^/\\]+)$") + + local isAllowed = #process.args == 0 + or table.find(allowedTests, test) + or table.find(allowedTests, withoutExt) + or table.find(allowedTests, baseName) + or table.find(allowedTests, baseNameWithoutExt) + + local constructors = { + case = frktest.test.case, + suite = frktest.test.suite, + } + + if not isAllowed then + constructors.case = frktest.test.skip.case + constructors.suite = frktest.test.skip.suite + end + + require(`../../{test}`)( + setmetatable(constructors, { __index = frktest.test }) + ) +end + +reporter.init() +process.exit(tonumber(frktest.run())) diff --git a/.lune/tests/reporter.luau b/.lune/tests/reporter.luau new file mode 100644 index 0000000..ac0cd82 --- /dev/null +++ b/.lune/tests/reporter.luau @@ -0,0 +1,69 @@ +local stdio = require("@lune/stdio") + +local frktest = require("../../lune_packages/frktest") +local Reporter = frktest._reporters.lune_console_reporter + +local watch = require("../lib/channel") + +local STYLE = table.freeze({ + suite = function(name: string) + return `{stdio.style("bold")}{stdio.color("purple")}SUITE{stdio.style( + "reset" + )} {name}` + end, + + report = function( + name: string, + state: "success" | "error" | "skip", + elapsed: number + ) + local state_color: stdio.Color = if state == "success" + then "green" + elseif state == "error" then "red" + elseif state == "skip" then "yellow" + else error("Invalid test state") + return ` {stdio.style("bold")}{stdio.color(state_color)}{if state + == "skip" + then "SKIP" + else "TEST"}{stdio.style("reset")} {name} [{stdio.style("dim")}{string.format( + "%.2fms", + elapsed + )}{stdio.style("reset")}]` + end, +}) + +local ReporterExt = {} +function ReporterExt.init() + frktest.test.on_suite_enter(function(suite) + print(STYLE.suite(suite.name)) + end) + + frktest.test.on_suite_leave(function() + stdio.write("\n") + end) + + local send_ts, recv_ts = watch((nil :: any) :: number) + + frktest.test.on_test_enter(function() + -- Send over some high precision timestamp when the test starts + return send_ts(os.clock()) + end) + + frktest.test.on_test_leave(function(test) + print(STYLE.report( + test.name, + if test.failed then "error" else "success", + + -- Await receival of the timestamp and convert the difference to ms + (os.clock() - recv_ts()) * 1000 + )) + end) + + frktest.test.on_test_skipped(function(test) + print(STYLE.report(test.name, "skip", 0)) + end) + + Reporter.init() +end + +return setmetatable(ReporterExt, { __index = Reporter }) diff --git a/.lune/typecheck.luau b/.lune/typecheck.luau new file mode 100644 index 0000000..3ee89d9 --- /dev/null +++ b/.lune/typecheck.luau @@ -0,0 +1,16 @@ +--> Run luau-lsp analysis to check for type errors + +local process = require("@lune/process") + +local CommandBuilder = require("./lib/exec") + +process.exit( + CommandBuilder.new("luau-lsp") + :withArg("analyze") + :withArgs({ "--settings", ".vscode/settings.json" }) + :withArgs({ "--ignore", "'**/.pesde/**'" }) + :withArgs({ "--ignore", "'./test-files/**'" }) + :withArg(".") + :withStdioStrategy("forward") + :exec().code +) diff --git a/.pesde/.gitkeep b/.pesde/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 0000000..4b17827 --- /dev/null +++ b/.styluaignore @@ -0,0 +1 @@ +!.lune/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..afb4651 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "luau-lsp.require.mode": "relativeToFile", + "luau-lsp.require.directoryAliases": { + "@lune/": "~/.lune/.typedefs/0.8.9/" + }, + "stylua.targetReleaseVersion": "latest" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f4d84c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 pesde-pkg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 85a7e80..39e8e38 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,38 @@ -# pesde scripts +# pesde/scripts_core -This repository hosts scripts & utilities for [pesde](https://github.com/daimond113/pesde) +Discord +Lune +pesde/scripts_core + + + + +Scripts and other utilities for use with pesde. + +## Prerequisites + +To ensure proper functionality, please make sure you have the following dependencies installed: + +- **pesde**: Version `>= 0.5.0-rc.15` +- **lune**: Version `>= 0.8.9` + +## Usage + +For example, to install Rojo scripts, run: + +```sh +# Add the scripts themselves +pesde add --dev pesde/scripts_rojo --target lune +# Add the Rojo CLI +pesde add --dev pesde/rojo --target lune +# Install dependencies +pesde install +``` + +If a sync tool you would like is not present here, please open an issue or submit a PR, following the format of one of the existing tools. + + + +# License + +This project is licensed under the [MIT](https://github.com/pesde-pkg/scripts/blob/master/LICENSE) license. diff --git a/lune/rojo/roblox_sync_config_generator.luau b/lune/rojo/roblox_sync_config_generator.luau deleted file mode 100644 index 3631255..0000000 --- a/lune/rojo/roblox_sync_config_generator.luau +++ /dev/null @@ -1,44 +0,0 @@ -local fs = require("@lune/fs") -local process = require("@lune/process") -local serde = require("@lune/serde") - -local package_directory = process.args[1] - -if fs.isFile(package_directory .. "/default.project.json") then - return -end - -local output = { - tree = {}, -} - -for i, file in process.args do - if i == 1 then - continue - end - - local name = string.gsub(file, ".luau?$", "") - - if name == "init" then - output.tree["$path"] = file - continue - end - - output.tree[name] = { - ["$path"] = file, - } -end - -if not output.tree["$path"] then - output.tree["$className"] = "Folder" -end - -if not output.tree["roblox_packages"] then - output.tree["roblox_packages"] = { - ["$path"] = { - optional = "roblox_packages", - }, - } -end - -fs.writeFile(package_directory .. "/default.project.json", serde.encode("json", output, true)) diff --git a/lune/rojo/sourcemap_generator.luau b/lune/rojo/sourcemap_generator.luau deleted file mode 100644 index 4cf09cd..0000000 --- a/lune/rojo/sourcemap_generator.luau +++ /dev/null @@ -1,19 +0,0 @@ -local fs = require("@lune/fs") -local process = require("@lune/process") -local serde = require("@lune/serde") -local stdio = require("@lune/stdio") - -local package_directory = process.args[1] - -if fs.isFile(package_directory .. "/default.project.json") then - process.spawn("rojo", { "sourcemap", package_directory }, { cwd = process.cwd, stdio = "forward" }) -elseif fs.isFile(package_directory .. "/init.luau") then - local sourcemap = { filePaths = { "init.luau" } } - stdio.write(serde.encode("json", sourcemap, false)) -elseif fs.isFile(package_directory .. "/init.lua") then - local sourcemap = { filePaths = { "init.lua" } } - stdio.write(serde.encode("json", sourcemap, false)) -else - -- use stderr to avoid this being parsed as the output of the sourcemap command - stdio.ewrite("no default.project.json found in " .. package_directory) -end diff --git a/pesde.lock b/pesde.lock new file mode 100644 index 0000000..aa705e2 --- /dev/null +++ b/pesde.lock @@ -0,0 +1,249 @@ +name = "pesde/scripts_core" +version = "0.0.1" +target = "lune" + +[workspace."pesde/scripts_core"] +lune = "" + +[workspace."pesde/scripts_rojo"] +lune = ".pesde/rojo" + +[graph."0x5eal/semver"."0.1.1 luau"] +resolved_ty = "peer" + +[graph."0x5eal/semver"."0.1.1 luau".target] +environment = "luau" +lib = "lib/init.luau" + +[graph."0x5eal/semver"."0.1.1 luau".dependencies] +"lukadev_0/option" = ["1.2.0 luau", "option"] +"lukadev_0/result" = ["1.2.0 luau", "result"] + +[graph."0x5eal/semver"."0.1.1 luau".pkg_ref] +ref_ty = "pesde" +name = "0x5eal/semver" +version = "0.1.1" +index_url = "https://github.com/daimond113/pesde-index" + +[graph."0x5eal/semver"."0.1.1 luau".pkg_ref.dependencies] +frktest = [{ name = "itsfrank/frktest", version = "^0.0.2", index = "https://github.com/daimond113/pesde-index", target = "lune" }, "dev"] +option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"] +result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"] + +[graph."0x5eal/semver"."0.1.1 luau".pkg_ref.target] +environment = "luau" +lib = "lib/init.luau" + +[graph."itsfrank/frktest"."0.0.2 lune"] +direct = ["frktest", { name = "itsfrank/frktest", version = "^0.0.2" }, "dev"] +resolved_ty = "dev" + +[graph."itsfrank/frktest"."0.0.2 lune".target] +environment = "lune" +lib = "src/_pesde_init.luau" + +[graph."itsfrank/frktest"."0.0.2 lune".pkg_ref] +ref_ty = "pesde" +name = "itsfrank/frktest" +version = "0.0.2" +index_url = "https://github.com/daimond113/pesde-index" + +[graph."itsfrank/frktest"."0.0.2 lune".pkg_ref.target] +environment = "lune" +lib = "src/_pesde_init.luau" + +[graph."jiwonz/dirs"."0.1.2 lune"] +resolved_ty = "standard" + +[graph."jiwonz/dirs"."0.1.2 lune".target] +environment = "lune" +lib = "src/init.luau" + +[graph."jiwonz/dirs"."0.1.2 lune".dependencies] +"jiwonz/pathfs" = ["0.1.0 lune", "pathfs"] + +[graph."jiwonz/dirs"."0.1.2 lune".pkg_ref] +ref_ty = "pesde" +name = "jiwonz/dirs" +version = "0.1.2" +index_url = "https://github.com/daimond113/pesde-index" + +[graph."jiwonz/dirs"."0.1.2 lune".pkg_ref.dependencies] +pathfs = [{ name = "jiwonz/pathfs", version = "^0.1.0", index = "https://github.com/daimond113/pesde-index" }, "standard"] + +[graph."jiwonz/dirs"."0.1.2 lune".pkg_ref.target] +environment = "lune" +lib = "src/init.luau" + +[graph."jiwonz/pathfs"."0.1.0 lune"] +direct = ["pathfs", { name = "jiwonz/pathfs", version = "^0.1.0" }, "dev"] +resolved_ty = "dev" + +[graph."jiwonz/pathfs"."0.1.0 lune".target] +environment = "lune" +lib = "init.luau" + +[graph."jiwonz/pathfs"."0.1.0 lune".pkg_ref] +ref_ty = "pesde" +name = "jiwonz/pathfs" +version = "0.1.0" +index_url = "https://github.com/daimond113/pesde-index" + +[graph."jiwonz/pathfs"."0.1.0 lune".pkg_ref.target] +environment = "lune" +lib = "init.luau" + +[graph."lukadev_0/option"."1.2.0 lune"] +resolved_ty = "standard" + +[graph."lukadev_0/option"."1.2.0 lune".target] +environment = "lune" +lib = "lib/init.luau" + +[graph."lukadev_0/option"."1.2.0 lune".pkg_ref] +ref_ty = "pesde" +name = "lukadev_0/option" +version = "1.2.0" +index_url = "https://github.com/daimond113/pesde-index" + +[graph."lukadev_0/option"."1.2.0 lune".pkg_ref.target] +environment = "lune" +lib = "lib/init.luau" + +[graph."lukadev_0/option"."1.2.0 luau"] +resolved_ty = "peer" + +[graph."lukadev_0/option"."1.2.0 luau".target] +environment = "luau" +lib = "lib/init.luau" + +[graph."lukadev_0/option"."1.2.0 luau".pkg_ref] +ref_ty = "pesde" +name = "lukadev_0/option" +version = "1.2.0" +index_url = "https://github.com/daimond113/pesde-index" + +[graph."lukadev_0/option"."1.2.0 luau".pkg_ref.target] +environment = "luau" +lib = "lib/init.luau" + +[graph."lukadev_0/result"."1.2.0 lune"] +resolved_ty = "standard" + +[graph."lukadev_0/result"."1.2.0 lune".target] +environment = "lune" +lib = "lib/init.luau" + +[graph."lukadev_0/result"."1.2.0 lune".pkg_ref] +ref_ty = "pesde" +name = "lukadev_0/result" +version = "1.2.0" +index_url = "https://github.com/daimond113/pesde-index" + +[graph."lukadev_0/result"."1.2.0 lune".pkg_ref.target] +environment = "lune" +lib = "lib/init.luau" + +[graph."lukadev_0/result"."1.2.0 luau"] +resolved_ty = "peer" + +[graph."lukadev_0/result"."1.2.0 luau".target] +environment = "luau" +lib = "lib/init.luau" + +[graph."lukadev_0/result"."1.2.0 luau".pkg_ref] +ref_ty = "pesde" +name = "lukadev_0/result" +version = "1.2.0" +index_url = "https://github.com/daimond113/pesde-index" + +[graph."lukadev_0/result"."1.2.0 luau".pkg_ref.target] +environment = "luau" +lib = "lib/init.luau" + +[graph."pesde/luau_lsp"."1.36.0 lune"] +direct = ["luau-lsp", { name = "pesde/luau_lsp", version = "^1.36.0" }, "dev"] +resolved_ty = "dev" + +[graph."pesde/luau_lsp"."1.36.0 lune".target] +environment = "lune" +bin = "init.luau" + +[graph."pesde/luau_lsp"."1.36.0 lune".dependencies] +"lukadev_0/option" = ["1.2.0 lune", "option"] +"lukadev_0/result" = ["1.2.0 lune", "result"] +"pesde/toolchainlib" = ["0.1.2 lune", "core"] + +[graph."pesde/luau_lsp"."1.36.0 lune".pkg_ref] +ref_ty = "pesde" +name = "pesde/luau_lsp" +version = "1.36.0" +index_url = "https://github.com/pesde-pkg/index" + +[graph."pesde/luau_lsp"."1.36.0 lune".pkg_ref.dependencies] +core = [{ name = "pesde/toolchainlib", version = "^0.1.2", index = "https://github.com/daimond113/pesde-index", target = "lune" }, "standard"] +option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "standard"] +result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "standard"] + +[graph."pesde/luau_lsp"."1.36.0 lune".pkg_ref.target] +environment = "lune" +bin = "init.luau" + +[graph."pesde/stylua"."2.0.1 lune"] +direct = ["stylua", { name = "pesde/stylua", version = "^2.0.1" }, "dev"] +resolved_ty = "dev" + +[graph."pesde/stylua"."2.0.1 lune".target] +environment = "lune" +bin = "init.luau" + +[graph."pesde/stylua"."2.0.1 lune".dependencies] +"lukadev_0/option" = ["1.2.0 lune", "option"] +"lukadev_0/result" = ["1.2.0 lune", "result"] +"pesde/toolchainlib" = ["0.1.2 lune", "core"] + +[graph."pesde/stylua"."2.0.1 lune".pkg_ref] +ref_ty = "pesde" +name = "pesde/stylua" +version = "2.0.1" +index_url = "https://github.com/pesde-pkg/index" + +[graph."pesde/stylua"."2.0.1 lune".pkg_ref.dependencies] +core = [{ name = "pesde/toolchainlib", version = "^0.1.0", index = "https://github.com/daimond113/pesde-index", target = "lune" }, "standard"] +option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "standard"] +result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "standard"] + +[graph."pesde/stylua"."2.0.1 lune".pkg_ref.target] +environment = "lune" +bin = "init.luau" + +[graph."pesde/toolchainlib"."0.1.2 lune"] +resolved_ty = "standard" + +[graph."pesde/toolchainlib"."0.1.2 lune".target] +environment = "lune" +lib = "src/init.luau" + +[graph."pesde/toolchainlib"."0.1.2 lune".dependencies] +"0x5eal/semver" = ["0.1.1 luau", "semver"] +"jiwonz/dirs" = ["0.1.2 lune", "dirs"] +"jiwonz/pathfs" = ["0.1.0 lune", "pathfs"] +"lukadev_0/option" = ["1.2.0 lune", "option"] +"lukadev_0/result" = ["1.2.0 lune", "result"] + +[graph."pesde/toolchainlib"."0.1.2 lune".pkg_ref] +ref_ty = "pesde" +name = "pesde/toolchainlib" +version = "0.1.2" +index_url = "https://github.com/daimond113/pesde-index" + +[graph."pesde/toolchainlib"."0.1.2 lune".pkg_ref.dependencies] +dirs = [{ name = "jiwonz/dirs", version = "^0.1.1", index = "https://github.com/daimond113/pesde-index" }, "standard"] +option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"] +pathfs = [{ name = "jiwonz/pathfs", version = "^0.1.0", index = "https://github.com/daimond113/pesde-index" }, "standard"] +result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"] +semver = [{ name = "0x5eal/semver", version = "^0.1.1", index = "https://github.com/daimond113/pesde-index", target = "luau" }, "peer"] + +[graph."pesde/toolchainlib"."0.1.2 lune".pkg_ref.target] +environment = "lune" +lib = "src/init.luau" diff --git a/pesde.toml b/pesde.toml new file mode 100644 index 0000000..407ac9d --- /dev/null +++ b/pesde.toml @@ -0,0 +1,36 @@ +name = "pesde/scripts_core" +version = "0.0.1" +pesde_version = "0.5.0-rc.16" +description = "Scripts and other utilities for use with pesde" +authors = [ + "dai (https://www.daimond113.com/)", + "Erica Marigold ", +] +repository = "https://github.com/pesde-pkg/scripts" +license = "MIT" +includes = [ + "src/**/*.luau", + "!src/**/*.spec.luau", + "pesde.toml", + "README.md", + "LICENSE", +] + +workspace_members = [".", ".pesde/rojo"] + +[meta.scripts.rojo] +version = "0.1.0" +tool_dependencies = { rojo = { name = "pesde/rojo", version = "^7.4.4" } } + +[target] +environment = "lune" +lib = "src/init.luau" + +[dev_dependencies] +frktest = { name = "itsfrank/frktest", version = "^0.0.2" } +pathfs = { name = "jiwonz/pathfs", version = "^0.1.0" } +luau-lsp = { name = "pesde/luau_lsp", version = "^1.36.0" } +stylua = { name = "pesde/stylua", version = "^2.0.1" } + +[indices] +default = "https://github.com/pesde-pkg/index" diff --git a/src/generators/rojo/sourcemap.luau b/src/generators/rojo/sourcemap.luau new file mode 100644 index 0000000..7404cad --- /dev/null +++ b/src/generators/rojo/sourcemap.luau @@ -0,0 +1,52 @@ +local fs = require("@lune/fs") +local process = require("@lune/process") +local serde = require("@lune/serde") +local stdio = require("@lune/stdio") + +local PLATFORM_SEP = if process.os == "windows" then "\\" else "/" + +-- A mapping of things to do depending on the file present +local PATH_ACTION_MAP: { [string]: (dir: string) -> number? } = { + ["default.project.json"] = function(dir) + return process.spawn("rojo", { "sourcemap", dir }, { + cwd = process.cwd, + env = process.env, + stdio = "forward", + }).code + end, + + ["init.lua"] = function() + return stdio.write( + serde.encode("json", { filePaths = { "init.lua" } }, false) + ) + end, + ["init.luau"] = function() + return stdio.write( + serde.encode("json", { filePaths = { "init.luau" } }, false) + ) + end, +} + +--- Writes a Rojo sourcemap for the project in the provided directory or the current +--- working directory to standard output. + +--- ## Errors +--- * The current process lacks permissions to a file +--- * Failure to spawn `rojo` command +--- * Any I/O error occurs +return function(packageDirectory: string?): boolean + packageDirectory = packageDirectory or process.cwd + + -- We go through the action mappings in order of priority and check for the + -- file predicates, if present, we execute the action and report our status + for path, action in PATH_ACTION_MAP do + if fs.isFile(`{packageDirectory}{PLATFORM_SEP}{path}`) then + local status = action(packageDirectory) + return if status ~= nil then status == 0 else true + end + end + + -- If we reached so far, that must mean none of the file predicates matched, + -- so we return a `false` signifying an error + return false +end diff --git a/src/generators/rojo/sourcemap.spec.luau b/src/generators/rojo/sourcemap.spec.luau new file mode 100644 index 0000000..7f7b497 --- /dev/null +++ b/src/generators/rojo/sourcemap.spec.luau @@ -0,0 +1,106 @@ +local fs = require("@lune/fs") +local luau = require("@lune/luau") +local process = require("@lune/process") +local regex = require("@lune/regex") +local serde = require("@lune/serde") + +local frktest = require("../../../lune_packages/frktest") +local check = frktest.assert.check + +local TEST_PROJECTS_DIR = "./test-files/rojo/test-projects" +local TEST_PROJECT_EXCLUDES = { + "json_model", + "bad_json_model", + "plugins", + "legacy-0.5.x-unknown-names", +} + +local BUILTIN_PATCHES: { + [string]: { [string]: (...any) -> ...any? }, +} = { + ["@lune/process"] = { + spawn = function( + program: string, + params: { string }, + options: process.SpawnOptions + ): process.SpawnResult + local patchedOptions: process.SpawnOptions = options or {} + patchedOptions.stdio = "default" + + local result = process.spawn(program, params, patchedOptions) + + -- First we make sure the command exited properly + assert( + result.ok, + `Expected \`rojo\` command to not error, got:\n{string.rep( + " ", + 6 + )}{result.stderr}` + ) + + -- We also make sure that the output JSON was valid + serde.decode("json", result.stdout) + + return result + end, + }, + + ["@lune/stdio"] = { + write = function(msg: string) + -- Only make sure output JSON is valid + serde.decode("json", msg) + end, + }, +} + +local function requireWithPatches(path: string) + for builtin, patch in BUILTIN_PATCHES do + if path == builtin then + return setmetatable(patch, { __index = require(builtin) }) + end + end + + return require(path) +end + +return function(test: typeof(frktest.test)) + test.suite( + "Generates Rojo sourcemaps for test projects successfully", + function() + for _, file in fs.readDir(TEST_PROJECTS_DIR) do + if table.find(TEST_PROJECT_EXCLUDES, file) then + -- It does not make sense to test sourcemap generation for some of the test projects + continue + end + + local testProject = `{TEST_PROJECTS_DIR}/{file}` + test.case(file, function() + -- If a file starts with `bad_` but not `bad_meta_`, we should expect a failure + -- Also, sorry about this shitty regex, regex-rs does not support look-around :( + local isBadMeta = regex.new( + "^bad_[^m]|^bad_m[^e]|^bad_me[^t]|^bad_met[^a]" + ) + local expect = if isBadMeta:isMatch(file) + then check.should_error + else check.should_not_error + + expect(function() + -- We override the require which applies some patches to some builtins, mainly preventing + -- command stdout forwarding and `stdio.write` outputs to stdout in order to not fill + -- test ouput with large amounts of JSON data + local sourcemap: (string) -> boolean = luau.load( + fs.readFile("./src/generators/rojo/sourcemap.luau"), + { + environment = { + require = requireWithPatches, + }, + } + )() + + return check.is_true(sourcemap(testProject)) + end) + end) + end + end + ) +end diff --git a/src/generators/rojo/sync_config.luau b/src/generators/rojo/sync_config.luau new file mode 100644 index 0000000..2048bba --- /dev/null +++ b/src/generators/rojo/sync_config.luau @@ -0,0 +1,115 @@ +local fs = require("@lune/fs") +local process = require("@lune/process") +local serde = require("@lune/serde") + +export type TreeProperties = { + Name: never?, + Parent: never?, +} + +export type TreeBase = { + ["$className"]: string?, + ["$ignoreUnknownInstances"]: boolean?, + ["$path"]: string | { optional: string }?, + ["$properties"]: TreeProperties?, +} + +export type TreeNormal = TreeBase & { + [string]: TreeNormal, +} & ({ ["$className"]: string } | { + ["$path"]: string | { optional: string }, +}) + +export type TreeService = TreeBase & { + [string]: TreeNormal, +} + +export type DataModelTree = TreeBase & { + StarterPlayer: (TreeBase & { + StarterPlayerScripts: TreeService?, + StarterCharacterScripts: TreeService?, + [string]: TreeNormal, + })?, + [string]: TreeService, +} + +export type Tree = (DataModelTree & { + ["$className"]: "DataModel", +}) | TreeNormal + +export type SyncConfig = { + name: string, + servePort: number?, + servePlaceIds: { number }?, + placeId: number?, + gameId: number?, + serveAddress: string?, + globIgnorePaths: { string }?, + tree: Tree, +} + +local PLATFORM_SEP = if process.os == "windows" then "\\" else "/" + +--- Generates a Rojo sync configuration file (`default.project.json`) from a list of +--- input files to be included. + +--- ## Errors +--- * The current process lacks permissions to a file +--- * Any I/O error occurs +return function( + packageDirectory: string?, + files: { string }, + options: { + writeToFile: boolean?, + force: boolean?, + } +): (boolean, string?) + packageDirectory = packageDirectory or process.cwd + local syncConfigPath = + `{packageDirectory}{PLATFORM_SEP}default.project.json` + if fs.isFile(syncConfigPath) and not options.force then + return true, nil + end + + local syncConfigTree = {} :: Tree + + for _, file in files do + -- Remove the `.lua` or `.luau` file extension from the file name + local name = string.gsub(file, ".luau?$", "") + + if name == "init" then + syncConfigTree["$path"] = name + continue + end + + syncConfigTree[name] = { + ["$path"] = file, + } + end + + -- If there isn't a top level path, we mark the entire thing as a Folder + if not syncConfigTree["$path"] then + syncConfigTree["$className"] = "Folder" + end + + -- If the config tree does not include pesde's downloaded roblox dependencies + -- directory, we add it as an optional one for the future, once dependencies + -- are installed + if not syncConfigTree["roblox_packages"] then + syncConfigTree["roblox_packages"] = { + ["$path"] = { + optional = "roblox_packages", + }, + } + end + + -- Finally, we serialize the config to a JSON string and optionally write it + -- to the sync config path + local serializedConfig = + serde.encode("json", { tree = syncConfigTree }, true) + if options.writeToFile then + fs.writeFile(syncConfigPath, serializedConfig) + end + + return true, serializedConfig +end diff --git a/src/generators/rojo/sync_config.spec.luau b/src/generators/rojo/sync_config.spec.luau new file mode 100644 index 0000000..014d247 --- /dev/null +++ b/src/generators/rojo/sync_config.spec.luau @@ -0,0 +1,40 @@ +local fs = require("@lune/fs") +local serde = require("@lune/serde") + +local frktest = require("../../../lune_packages/frktest") +local check = frktest.assert.check + +local syncConfig = require("./sync_config") + +local TEST_PROJECTS_DIRS = { + "./test-files/rojo/test-projects", + "./test-files/rojo/rojo-test/serve-tests", +} + +return function(test: typeof(frktest.test)) + test.suite("Generates Rojo valid sync configs", function() + for _, dir in TEST_PROJECTS_DIRS do + for _, file in fs.readDir(dir) do + local fullPath = `{dir}/{file}` + test.case(`{file}`, function() + local ok, config = syncConfig( + fullPath, + fs.readDir(fullPath), + { writeToFile = false, force = true } + ) + check.is_true(ok) + + -- Make sure that the generated config and the real configs are similar + local generatedConfig, realConfig = + serde.decode("json", config), + serde.decode( + "json", + fs.readFile(`{fullPath}/default.project.json`) + ) + + check.table.contains(realConfig, generatedConfig) + end) + end + end + end) +end diff --git a/src/init.luau b/src/init.luau new file mode 100644 index 0000000..f0d77f3 --- /dev/null +++ b/src/init.luau @@ -0,0 +1,8 @@ +return { + generators = { + rojo = { + sourcemap = require("./generators/rojo/sourcemap"), + syncConfig = require("./generators/rojo/sync_config"), + }, + }, +} diff --git a/stylua.toml b/stylua.toml index 056139b..4821978 100644 --- a/stylua.toml +++ b/stylua.toml @@ -1,2 +1,10 @@ +line_endings = "Unix" +quote_style = "AutoPreferDouble" +indent_type = "Tabs" +call_parentheses = "Always" + +indent_width = 4 +column_width = 80 + [sort_requires] -enabled = true \ No newline at end of file +enabled = true diff --git a/test-files/rojo b/test-files/rojo new file mode 160000 index 0000000..b7d3394 --- /dev/null +++ b/test-files/rojo @@ -0,0 +1 @@ +Subproject commit b7d3394464e0ffb350b9c8481399cc5845c10f07