diff --git a/.lune/fmt.luau b/.lune/fmt.luau index aad2818..77be22a 100644 --- a/.lune/fmt.luau +++ b/.lune/fmt.luau @@ -1,7 +1,7 @@ ---> 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) +--> 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 index 03cf12c..3df65fc 100644 --- a/.lune/lib/channel.luau +++ b/.lune/lib/channel.luau @@ -1,48 +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 }, -} - ---- Crates 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 \ No newline at end of file +--- 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 }, +} + +--- Crates 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 index 8ce24da..9550729 100644 --- a/.lune/lib/exec.luau +++ b/.lune/lib/exec.luau @@ -1,103 +1,103 @@ ---> 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 +--> 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 index b2f1ede..a9398fa 100644 --- a/.lune/lib/manifest.luau +++ b/.lune/lib/manifest.luau @@ -1,71 +1,71 @@ -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 } - | { repo: string, rev: string, path: string? } -) & { - target: string?, -}) | { wally: string, version: string, index: string? } - -export type PackageTarget = { - environment: "luau" | "lune" | "roblox" | "roblox_server", - lib: string, -} | ({ environment: "luau" | "lune" } & ({ - 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 +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 } + | { repo: string, rev: string, path: string? } +) & { + target: string?, +}) | { wally: string, version: string, index: string? } + +export type PackageTarget = { + environment: "luau" | "lune" | "roblox" | "roblox_server", + lib: string, +} | ({ environment: "luau" | "lune" } & ({ + 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 index 8f6e7ba..054ad32 100644 --- a/.lune/tests/init.luau +++ b/.lune/tests/init.luau @@ -1,80 +1,79 @@ ---> 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())) \ No newline at end of file +--> 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 index 887851d..ea4160f 100644 --- a/.lune/tests/reporter.luau +++ b/.lune/tests/reporter.luau @@ -1,60 +1,59 @@ -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 }) +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 index 2818b79..3e56319 100644 --- a/.lune/typecheck.luau +++ b/.lune/typecheck.luau @@ -1,16 +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("~/.rokit/bin/luau-lsp") - :withArg("analyze") - :withArgs({ "--settings", ".vscode/settings.json" }) - :withArgs({ "--ignore", "'**/.pesde/**'" }) - :withArgs({ "--ignore", "'./test-files/**'" }) - :withArg(".") - :withStdioStrategy("forward") - :exec().code -) \ No newline at end of file +--> Run luau-lsp analysis to check for type errors + +local process = require("@lune/process") + +local CommandBuilder = require("./lib/exec") + +process.exit( + CommandBuilder.new("~/.rokit/bin/luau-lsp") + :withArg("analyze") + :withArgs({ "--settings", ".vscode/settings.json" }) + :withArgs({ "--ignore", "'**/.pesde/**'" }) + :withArgs({ "--ignore", "'./test-files/**'" }) + :withArg(".") + :withStdioStrategy("forward") + :exec().code +)