style: apply stylua formatter

This commit is contained in:
Erica Marigold 2024-12-08 18:30:38 +00:00
parent e907258396
commit ef8953fdcc
Signed by: DevComp
GPG key ID: 429EF1C337871656
13 changed files with 625 additions and 519 deletions

View file

@ -11,18 +11,18 @@ local pathfs = require("../lune_packages/pathfs")
type ToolChoice = "rojo" type ToolChoice = "rojo"
type ManifestExt = { type ManifestExt = {
scripts: { scripts: {
[ToolChoice]: { [ToolChoice]: {
version: string, version: string,
tool_dependencies: { { [string]: manifest.DependencySpecifier } }, tool_dependencies: { { [string]: manifest.DependencySpecifier } },
}, },
}, },
} }
local SCRIPTS_DIR = pathfs.getAbsolutePathOf(pathfs.Path.from(".pesde")) local SCRIPTS_DIR = pathfs.getAbsolutePathOf(pathfs.Path.from(".pesde"))
local MANIFEST = manifest(nil, (nil :: any) :: { meta: ManifestExt }) local MANIFEST = manifest(nil, (nil :: any) :: { meta: ManifestExt })
local SCRIPTS = { local SCRIPTS = {
syncConfigGenerator = [[local process = require("@lune/process") syncConfigGenerator = [[local process = require("@lune/process")
local args = table.clone(process.args) local args = table.clone(process.args)
local ok, _ = local ok, _ =
@ -31,7 +31,7 @@ if not ok then
return process.exit(1) return process.exit(1)
end]], end]],
sourcemapGenerator = [[local process = require("@lune/process") sourcemapGenerator = [[local process = require("@lune/process")
local ok = require("./lune_packages/core").generators.%s.sourcemap(process.args[1]) local ok = require("./lune_packages/core").generators.%s.sourcemap(process.args[1])
if not ok then if not ok then
@ -54,142 +54,171 @@ Prints out a sourcemap for Wally dependencies for type extraction.
]] ]]
local function logPrefix(type: "error" | "info") local function logPrefix(type: "error" | "info")
local statusColor: stdio.Color = if type == "error" local statusColor: stdio.Color = if type == "error"
then "red" then "red"
elseif type == "info" then "green" elseif type == "info" then "green"
else error(`Invalid type: {type}`) else error(`Invalid type: {type}`)
return `main::{stdio.style("bold")}{stdio.color(statusColor)}{type}{stdio.color("reset")}` return `main::{stdio.style("bold")}{stdio.color(statusColor)}{type}{stdio.color(
"reset"
)}`
end end
local INFO_PREFIX = `[ {logPrefix("info")}]` local INFO_PREFIX = `[ {logPrefix("info")}]`
local _ERROR_PREFIX = `[{logPrefix("error")}]` local _ERROR_PREFIX = `[{logPrefix("error")}]`
local function installDeps(): number local function installDeps(): number
local PESDE_INFO_PREFIX = string.gsub(`[{logPrefix("info")}]`, "main", "pesde") local PESDE_INFO_PREFIX =
local PESDE_ERROR_PREFIX = string.gsub(`[{logPrefix("error")}]`, "main", "pesde") string.gsub(`[{logPrefix("info")}]`, "main", "pesde")
local SPINNER_STATES = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" } local PESDE_ERROR_PREFIX =
string.gsub(`[{logPrefix("error")}]`, "main", "pesde")
local SPINNER_STATES =
{ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
stdio.write(`{PESDE_INFO_PREFIX} Installing dependencies with pesde`) stdio.write(`{PESDE_INFO_PREFIX} Installing dependencies with pesde`)
-- Spawn our thread to display the spinner -- Spawn our thread to display the spinner
local spinnerThread = task.spawn(function() local spinnerThread = task.spawn(function()
-- Hide the cursor -- Hide the cursor
stdio.write("\x1b[?25l") stdio.write("\x1b[?25l")
-- Display the spinner -- Display the spinner
while true do while true do
for _, state in SPINNER_STATES do for _, state in SPINNER_STATES do
stdio.write(` {state}`) stdio.write(` {state}`)
stdio.write( stdio.write(
-- Moves the cursor back 1 spot and clears everything after -- Moves the cursor back 1 spot and clears everything after
"\x1b[2D\x1b[0K" "\x1b[2D\x1b[0K"
) )
end end
end end
end) end)
-- `process_spawn` is an async rust function, so tokio spawns a hardware -- `process_spawn` is an async rust function, so tokio spawns a hardware
-- thread and mlua will yield the current thread, allowing for `spinnerThread` -- thread and mlua will yield the current thread, allowing for `spinnerThread`
-- to continue executing -- to continue executing
local child = process.spawn("pesde", { "install" }, { local child = process.spawn("pesde", { "install" }, {
stdio = "default", stdio = "default",
}) })
-- If we reach this point, that means mlua resumed the current thread and -- If we reach this point, that means mlua resumed the current thread and
-- `process_spawn` returned or errored. We can now close `spinnerThread` and -- `process_spawn` returned or errored. We can now close `spinnerThread` and
-- clean up -- clean up
task.cancel(spinnerThread) task.cancel(spinnerThread)
stdio.write( stdio.write(
-- Clear the current line, move cursor back to its beginning and -- Clear the current line, move cursor back to its beginning and
-- show it -- show it
"\x1b[2K\x1b[1G\x1b[?25h" "\x1b[2K\x1b[1G\x1b[?25h"
) )
if not child.ok then if not child.ok then
stdio.ewrite(`{PESDE_ERROR_PREFIX} Failed to install dependencies with pesde, error:\n`) stdio.ewrite(
stdio.ewrite(child.stderr) `{PESDE_ERROR_PREFIX} Failed to install dependencies with pesde, error:\n`
end )
stdio.ewrite(child.stderr)
end
stdio.write(`{PESDE_INFO_PREFIX} Installed dependencies with pesde successfully\n`) stdio.write(
`{PESDE_INFO_PREFIX} Installed dependencies with pesde successfully\n`
)
return child.code return child.code
end end
for tool, generators in lib.generators do for tool, generators in lib.generators do
local startTime = os.clock() local startTime = os.clock()
-- For each tool, we generate a respective manifests and scripts -- For each tool, we generate a respective manifests and scripts
local toolChoice = tool :: ToolChoice local toolChoice = tool :: ToolChoice
local toolScriptsDir = SCRIPTS_DIR:join(toolChoice) local toolScriptsDir = SCRIPTS_DIR:join(toolChoice)
local toolMeta = MANIFEST.meta.scripts[toolChoice] local toolMeta = MANIFEST.meta.scripts[toolChoice]
if not pathfs.isDir(toolScriptsDir) then if not pathfs.isDir(toolScriptsDir) then
pathfs.writeDir(toolScriptsDir) pathfs.writeDir(toolScriptsDir)
end end
local capitalisedToolChoice = string.gsub(toolChoice, "^%l", string.upper) local capitalisedToolChoice = string.gsub(toolChoice, "^%l", string.upper)
-- Define the manifest for the tool -- Define the manifest for the tool
local toolManifest: manifest.PesdeManifest = { local toolManifest: manifest.PesdeManifest = {
name = `pesde/scripts_{toolChoice}`, name = `pesde/scripts_{toolChoice}`,
version = toolMeta.version, version = toolMeta.version,
-- For the description, we capitalize the first letter of the tool name here -- For the description, we capitalize the first letter of the tool name here
-- since it is a proper noun -- since it is a proper noun
description = `Scripts for {capitalisedToolChoice}-based Roblox projects.`, description = `Scripts for {capitalisedToolChoice}-based Roblox projects.`,
authors = { authors = {
"daimond113 <contact@daimond113.com> (https://www.daimond113.com/)", "daimond113 <contact@daimond113.com> (https://www.daimond113.com/)",
"Erica Marigold <hi@devcomp.xyz>", "Erica Marigold <hi@devcomp.xyz>",
}, },
repository = "https://github.com/pesde-pkg/scripts", repository = "https://github.com/pesde-pkg/scripts",
license = "MIT", license = "MIT",
includes = { includes = {
"roblox_sync_config_generator.luau", "roblox_sync_config_generator.luau",
"sourcemap_generator.luau", "sourcemap_generator.luau",
"pesde.toml", "pesde.toml",
"README.md", "README.md",
"LICENSE", "LICENSE",
}, },
target = { target = {
environment = "lune", environment = "lune",
scripts = { scripts = {
roblox_sync_config_generator = "roblox_sync_config_generator.luau", roblox_sync_config_generator = "roblox_sync_config_generator.luau",
sourcemap_generator = "sourcemap_generator.luau", sourcemap_generator = "sourcemap_generator.luau",
}, },
}, },
peer_dependencies = toolMeta.tool_dependencies, peer_dependencies = toolMeta.tool_dependencies,
dependencies = { dependencies = {
core = { workspace = "pesde/scripts_core", version = "^" }, core = { workspace = "pesde/scripts_core", version = "^" },
}, },
indices = { indices = {
default = "https://github.com/pesde-pkg/index", default = "https://github.com/pesde-pkg/index",
}, },
} }
-- Format the scripts for the tool -- Format the scripts for the tool
local syncConfigGeneratorScript, sourcemapGeneratorScript = local syncConfigGeneratorScript, sourcemapGeneratorScript =
string.format(SCRIPTS.syncConfigGenerator, toolChoice), string.format(SCRIPTS.sourcemapGenerator, toolChoice) string.format(SCRIPTS.syncConfigGenerator, toolChoice),
string.format(SCRIPTS.sourcemapGenerator, toolChoice)
-- Finally, write all the generated files -- Finally, write all the generated files
pathfs.writeFile(toolScriptsDir:join("pesde.toml"), serde.encode("toml", toolManifest, true)) pathfs.writeFile(
pathfs.writeFile(toolScriptsDir:join("roblox_sync_config_generator.luau"), syncConfigGeneratorScript) toolScriptsDir:join("pesde.toml"),
pathfs.writeFile(toolScriptsDir:join("sourcemap_generator.luau"), sourcemapGeneratorScript) serde.encode("toml", toolManifest, true)
pathfs.writeFile( )
toolScriptsDir:join("README.md"), pathfs.writeFile(
string.format(README_TMPL, toolChoice, capitalisedToolChoice, capitalisedToolChoice) toolScriptsDir:join("roblox_sync_config_generator.luau"),
) syncConfigGeneratorScript
pathfs.copy(pathfs.cwd:join("LICENSE"), toolScriptsDir:join("LICENSE"), true) )
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( stdio.write(
`{INFO_PREFIX} Generated script project for tool {toolChoice} ({stdio.style("dim")}{string.format( `{INFO_PREFIX} Generated script project for tool {toolChoice} ({stdio.style(
"%.2fms", "dim"
(os.clock() - startTime) * 1000 )}{string.format("%.2fms", (os.clock() - startTime) * 1000)}{stdio.style(
)}{stdio.style("reset")})\n` "reset"
) )})\n`
)
end end
-- Now we install the dependencies for the newly created projects -- Now we install the dependencies for the newly created projects

View file

@ -4,4 +4,10 @@ local process = require("@lune/process")
local CommandBuilder = require("./lib/exec") local CommandBuilder = require("./lib/exec")
process.exit(CommandBuilder.new("stylua"):withArg("."):withArgs(process.args):withStdioStrategy("forward"):exec().code) process.exit(
CommandBuilder.new("stylua")
:withArg(".")
:withArgs(process.args)
:withStdioStrategy("forward")
:exec().code
)

View file

@ -11,38 +11,38 @@
--- end) --- end)
--- ``` --- ```
type Watch<T> = { type Watch<T> = {
value: T?, value: T?,
receivers: { thread }, receivers: { thread },
} }
--- Creates a new `Watch` channel, returning its send and receive handles. --- Creates a new `Watch` channel, returning its send and receive handles.
local function chan<T>(_phantom: T): ((T) -> (), () -> T?) local function chan<T>(_phantom: T): ((T) -> (), () -> T?)
local watch: Watch<T> = { local watch: Watch<T> = {
value = nil, value = nil,
receivers = {}, receivers = {},
} }
local function send(value: T) local function send(value: T)
watch.value = value watch.value = value
for _, receiver in watch.receivers do for _, receiver in watch.receivers do
coroutine.resume(receiver, value) coroutine.resume(receiver, value)
end end
end end
local function recv(): T local function recv(): T
local value = watch.value local value = watch.value
watch.value = nil watch.value = nil
if value == nil then if value == nil then
table.insert(watch.receivers, coroutine.running()) table.insert(watch.receivers, coroutine.running())
return coroutine.yield() return coroutine.yield()
end end
return value :: T return value :: T
end end
return send, recv return send, recv
end end
return chan return chan

View file

@ -5,99 +5,119 @@ local stdio = require("@lune/stdio")
local CommandBuilder = {} local CommandBuilder = {}
export type CommandBuilder = typeof(setmetatable({} :: CommandBuilderFields, { __index = CommandBuilder })) export type CommandBuilder = typeof(setmetatable(
{} :: CommandBuilderFields,
{ __index = CommandBuilder }
))
type CommandBuilderFields = { type CommandBuilderFields = {
program: string, program: string,
args: { string }, args: { string },
stdioStrategy: IoStrategyMapping?, stdioStrategy: IoStrategyMapping?,
} }
export type StdioStrategy = "pipe" | "forward" | "none" export type StdioStrategy = "pipe" | "forward" | "none"
export type IoStrategyMapping = { export type IoStrategyMapping = {
stdout: StdioStrategy?, stdout: StdioStrategy?,
stderr: StdioStrategy?, stderr: StdioStrategy?,
} }
local DEFAULT_STDIO_STRATEGY: IoStrategyMapping = { local DEFAULT_STDIO_STRATEGY: IoStrategyMapping = {
stdout = "pipe", stdout = "pipe",
stderr = "pipe", stderr = "pipe",
} }
function CommandBuilder.new(program: string) function CommandBuilder.new(program: string)
return setmetatable( return setmetatable(
{ {
program = program, program = program,
args = {}, args = {},
stdioStrategy = nil, stdioStrategy = nil,
} :: CommandBuilderFields, } :: CommandBuilderFields,
{ {
__index = CommandBuilder, __index = CommandBuilder,
} }
) )
end end
function CommandBuilder.withArg(self: CommandBuilder, arg: string): CommandBuilder function CommandBuilder.withArg(
table.insert(self.args, arg) self: CommandBuilder,
return self arg: string
): CommandBuilder
table.insert(self.args, arg)
return self
end end
function CommandBuilder.withArgs(self: CommandBuilder, args: { string }): CommandBuilder function CommandBuilder.withArgs(
for _, arg in args do self: CommandBuilder,
self:withArg(arg) args: { string }
end ): CommandBuilder
for _, arg in args do
self:withArg(arg)
end
return self return self
end end
function CommandBuilder.withStdioStrategy( function CommandBuilder.withStdioStrategy(
self: CommandBuilder, self: CommandBuilder,
strategy: StdioStrategy | IoStrategyMapping strategy: StdioStrategy | IoStrategyMapping
): CommandBuilder ): CommandBuilder
self.stdioStrategy = if typeof(strategy) == "string" self.stdioStrategy = if typeof(strategy) == "string"
then { then {
stdout = strategy, stdout = strategy,
stderr = strategy, stderr = strategy,
} }
else strategy else strategy
return self return self
end end
local function intoSpawnOptionsStdioKind(strategy: StdioStrategy): process.SpawnOptionsStdioKind local function intoSpawnOptionsStdioKind(
if strategy == "pipe" then strategy: StdioStrategy
return "default" ): process.SpawnOptionsStdioKind
end if strategy == "pipe" then
return "default"
end
if strategy == "forward" then if strategy == "forward" then
return "forward" return "forward"
end end
if strategy == "none" then if strategy == "none" then
return "none" return "none"
end end
error(`Non-strategy provided: {strategy}`) error(`Non-strategy provided: {strategy}`)
end end
function CommandBuilder.exec(self: CommandBuilder): process.SpawnResult function CommandBuilder.exec(self: CommandBuilder): process.SpawnResult
print("$", stdio.style("dim") .. self.program, table.concat(self.args, " ") .. stdio.style("reset")) print(
"$",
stdio.style("dim") .. self.program,
table.concat(self.args, " ") .. stdio.style("reset")
)
local function translateIoStrategyMappings(mappings: IoStrategyMapping) local function translateIoStrategyMappings(mappings: IoStrategyMapping)
local translatedMappings: process.SpawnOptionsStdio = {} local translatedMappings: process.SpawnOptionsStdio = {}
for field, value in mappings do for field, value in mappings do
translatedMappings[field] = intoSpawnOptionsStdioKind(value) translatedMappings[field] = intoSpawnOptionsStdioKind(value)
end end
return translatedMappings return translatedMappings
end end
local child = process.spawn(self.program, self.args, { local child = process.spawn(self.program, self.args, {
shell = true, shell = true,
stdio = translateIoStrategyMappings(self.stdioStrategy or DEFAULT_STDIO_STRATEGY), stdio = translateIoStrategyMappings(
}) self.stdioStrategy or DEFAULT_STDIO_STRATEGY
),
})
if not child.ok then if not child.ok then
print(`\n{stdio.color("red")}[luau-lsp]{stdio.color("reset")} Exited with code`, child.code) print(
end `\n{stdio.color("red")}[luau-lsp]{stdio.color("reset")} Exited with code`,
child.code
)
end
return child return child
end end
return CommandBuilder return CommandBuilder

View file

@ -2,72 +2,79 @@ local fs = require("@lune/fs")
local serde = require("@lune/serde") local serde = require("@lune/serde")
export type SPDXLicense = export type SPDXLicense =
"MIT" "MIT"
| "Apache-2.0" | "Apache-2.0"
| "BSD-2-Clause" | "BSD-2-Clause"
| "BSD-3-Clause" | "BSD-3-Clause"
| "GPL-2.0" | "GPL-2.0"
| "GPL-3.0" | "GPL-3.0"
| "LGPL-2.1" | "LGPL-2.1"
| "LGPL-3.0" | "LGPL-3.0"
| "MPL-2.0" | "MPL-2.0"
| "ISC" | "ISC"
| "Unlicense" | "Unlicense"
| "WTFPL" | "WTFPL"
| "Zlib" | "Zlib"
| "CC0-1.0" | "CC0-1.0"
| "CC-BY-4.0" | "CC-BY-4.0"
| "CC-BY-SA-4.0" | "CC-BY-SA-4.0"
| "BSL-1.0" | "BSL-1.0"
| "EPL-2.0" | "EPL-2.0"
| "AGPL-3.0" | "AGPL-3.0"
export type DependencySpecifier = (( export type DependencySpecifier = (({
{ name: string, version: string, index: string? } name: string,
| { workspace: string, version: string } version: string,
) & { index: string?,
target: string?, } | { workspace: string, version: string }) & {
}) target: string?,
| { wally: string, version: string, index: string? } }) | {
| { repo: string, rev: string, path: string? } wally: string,
version: string,
index: string?,
} | {
repo: string,
rev: string,
path: string?,
}
export type PackageTarget = { export type PackageTarget = {
environment: "roblox" | "roblox_server", environment: "roblox" | "roblox_server",
lib: string, lib: string,
} | { } | {
environment: "luau" | "lune", environment: "luau" | "lune",
lib: string, lib: string,
bin: string, bin: string,
scripts: { [string]: string }, scripts: { [string]: string },
} }
export type PesdeManifest<T = {}> = { export type PesdeManifest<T = {}> = {
name: string, name: string,
version: string, version: string,
description: string?, description: string?,
license: SPDXLicense?, license: SPDXLicense?,
authors: { string }?, authors: { string }?,
repository: string?, repository: string?,
private: boolean?, private: boolean?,
includes: { string }?, includes: { string }?,
pesde_version: string?, pesde_version: string?,
workspace_members: { string }?, workspace_members: { string }?,
target: PackageTarget, target: PackageTarget,
build_files: { string }?, build_files: { string }?,
scripts: { [string]: string }?, scripts: { [string]: string }?,
indices: { [string]: string }, indices: { [string]: string },
wally_indices: { [string]: string }?, wally_indices: { [string]: string }?,
overrides: { [string]: DependencySpecifier }?, overrides: { [string]: DependencySpecifier }?,
patches: { [string]: { [string]: string } }?, patches: { [string]: { [string]: string } }?,
place: { [string]: string }?, place: { [string]: string }?,
dependencies: { [string]: DependencySpecifier }?, dependencies: { [string]: DependencySpecifier }?,
peer_dependencies: { [string]: DependencySpecifier }?, peer_dependencies: { [string]: DependencySpecifier }?,
dev_dependencies: { [string]: DependencySpecifier }?, dev_dependencies: { [string]: DependencySpecifier }?,
} & T } & T
return function<T>(path: string?, _phantom: T): PesdeManifest<T> return function<T>(path: string?, _phantom: T): PesdeManifest<T>
local manifestContents = fs.readFile(path or "pesde.toml") local manifestContents = fs.readFile(path or "pesde.toml")
local decoded = serde.decode("toml", manifestContents) local decoded = serde.decode("toml", manifestContents)
return decoded :: PesdeManifest<T> return decoded :: PesdeManifest<T>
end end

View file

@ -10,69 +10,71 @@ local reporter = require("./reporter")
-- A more proper solution would be to use luau.load instead, but -- A more proper solution would be to use luau.load instead, but
-- frktest requires its global state to be modified by test suites -- frktest requires its global state to be modified by test suites
local require = require :: ( local require = require :: (
path: string path: string
) -> ( ) -> (
test: typeof(setmetatable( test: typeof(setmetatable(
{} :: { {} :: {
case: (name: string, fn: () -> nil) -> (), case: (name: string, fn: () -> nil) -> (),
suite: (name: string, fn: () -> ()) -> (), suite: (name: string, fn: () -> ()) -> (),
}, },
{ __index = frktest.test } { __index = frktest.test }
)) ))
) -> () ) -> ()
local function discoverTests(dir: string) local function discoverTests(dir: string)
local tests = {} local tests = {}
for _, file in fs.readDir(dir) do for _, file in fs.readDir(dir) do
local fullPath = `{dir}/{file}` local fullPath = `{dir}/{file}`
-- Look for files ending in `.spec.luau` as tests -- Look for files ending in `.spec.luau` as tests
if fs.isFile(fullPath) and string.sub(file, -10) == ".spec.luau" then if fs.isFile(fullPath) and string.sub(file, -10) == ".spec.luau" then
table.insert(tests, fullPath) table.insert(tests, fullPath)
end end
-- Recurse for directories -- Recurse for directories
if fs.isDir(fullPath) then if fs.isDir(fullPath) then
local moreTests = discoverTests(fullPath) local moreTests = discoverTests(fullPath)
-- Why are the indices starting at 0???? What???? -- Why are the indices starting at 0???? What????
table.move(moreTests, 0, #moreTests, #tests, tests) table.move(moreTests, 0, #moreTests, #tests, tests)
end end
end end
return tests return tests
end end
local allowedTests = process.args local allowedTests = process.args
for _, test in discoverTests("src") do for _, test in discoverTests("src") do
-- If we are given any arguments, we only run those tests, otherwise, -- If we are given any arguments, we only run those tests, otherwise,
-- we run all the tests -- we run all the tests
-- So, to include only a certain set of test files, you can provide either -- 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 full path of the test file or name of the test file, with or without
-- the `.spec.luau` extension -- the `.spec.luau` extension
local baseName = string.match(test, "([^/\\]+)$") local baseName = string.match(test, "([^/\\]+)$")
local withoutExt = string.sub(test, 1, -11) local withoutExt = string.sub(test, 1, -11)
local baseNameWithoutExt = string.match(withoutExt, "([^/\\]+)$") local baseNameWithoutExt = string.match(withoutExt, "([^/\\]+)$")
local isAllowed = #process.args == 0 local isAllowed = #process.args == 0
or table.find(allowedTests, test) or table.find(allowedTests, test)
or table.find(allowedTests, withoutExt) or table.find(allowedTests, withoutExt)
or table.find(allowedTests, baseName) or table.find(allowedTests, baseName)
or table.find(allowedTests, baseNameWithoutExt) or table.find(allowedTests, baseNameWithoutExt)
local constructors = { local constructors = {
case = frktest.test.case, case = frktest.test.case,
suite = frktest.test.suite, suite = frktest.test.suite,
} }
if not isAllowed then if not isAllowed then
constructors.case = frktest.test.skip.case constructors.case = frktest.test.skip.case
constructors.suite = frktest.test.skip.suite constructors.suite = frktest.test.skip.suite
end end
require(`../../{test}`)(setmetatable(constructors, { __index = frktest.test })) require(`../../{test}`)(
setmetatable(constructors, { __index = frktest.test })
)
end end
reporter.init() reporter.init()

View file

@ -6,54 +6,64 @@ local Reporter = frktest._reporters.lune_console_reporter
local watch = require("../lib/channel") local watch = require("../lib/channel")
local STYLE = table.freeze({ local STYLE = table.freeze({
suite = function(name: string) suite = function(name: string)
return `{stdio.style("bold")}{stdio.color("purple")}SUITE{stdio.style("reset")} {name}` return `{stdio.style("bold")}{stdio.color("purple")}SUITE{stdio.style(
end, "reset"
)} {name}`
end,
report = function(name: string, state: "success" | "error" | "skip", elapsed: number) report = function(
local state_color: stdio.Color = if state == "success" name: string,
then "green" state: "success" | "error" | "skip",
elseif state == "error" then "red" elapsed: number
elseif state == "skip" then "yellow" )
else error("Invalid test state") local state_color: stdio.Color = if state == "success"
return ` {stdio.style("bold")}{stdio.color(state_color)}{if state == "skip" then "SKIP" else "TEST"}{stdio.style( then "green"
"reset" elseif state == "error" then "red"
)} {name} [{stdio.style("dim")}{string.format("%.2fms", elapsed)}{stdio.style("reset")}]` elseif state == "skip" then "yellow"
end, 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 = {} local ReporterExt = {}
function ReporterExt.init() function ReporterExt.init()
frktest.test.on_suite_enter(function(suite) frktest.test.on_suite_enter(function(suite)
print(STYLE.suite(suite.name)) print(STYLE.suite(suite.name))
end) end)
frktest.test.on_suite_leave(function() frktest.test.on_suite_leave(function()
stdio.write("\n") stdio.write("\n")
end) end)
local send_ts, recv_ts = watch((nil :: any) :: number) local send_ts, recv_ts = watch((nil :: any) :: number)
frktest.test.on_test_enter(function() frktest.test.on_test_enter(function()
-- Send over some high precision timestamp when the test starts -- Send over some high precision timestamp when the test starts
return send_ts(os.clock()) return send_ts(os.clock())
end) end)
frktest.test.on_test_leave(function(test) frktest.test.on_test_leave(function(test)
print(STYLE.report( print(STYLE.report(
test.name, test.name,
if test.failed then "error" else "success", if test.failed then "error" else "success",
-- Await receival of the timestamp and convert the difference to ms -- Await receival of the timestamp and convert the difference to ms
(os.clock() - recv_ts()) * 1000 (os.clock() - recv_ts()) * 1000
)) ))
end) end)
frktest.test.on_test_skipped(function(test) frktest.test.on_test_skipped(function(test)
print(STYLE.report(test.name, "skip", 0)) print(STYLE.report(test.name, "skip", 0))
end) end)
Reporter.init() Reporter.init()
end end
return setmetatable(ReporterExt, { __index = Reporter }) return setmetatable(ReporterExt, { __index = Reporter })

View file

@ -5,12 +5,12 @@ local process = require("@lune/process")
local CommandBuilder = require("./lib/exec") local CommandBuilder = require("./lib/exec")
process.exit( process.exit(
CommandBuilder.new("luau-lsp") CommandBuilder.new("luau-lsp")
:withArg("analyze") :withArg("analyze")
:withArgs({ "--settings", ".vscode/settings.json" }) :withArgs({ "--settings", ".vscode/settings.json" })
:withArgs({ "--ignore", "'**/.pesde/**'" }) :withArgs({ "--ignore", "'**/.pesde/**'" })
:withArgs({ "--ignore", "'./test-files/**'" }) :withArgs({ "--ignore", "'./test-files/**'" })
:withArg(".") :withArg(".")
:withStdioStrategy("forward") :withStdioStrategy("forward")
:exec().code :exec().code
) )

View file

@ -7,20 +7,24 @@ local PLATFORM_SEP = if process.os == "windows" then "\\" else "/"
-- A mapping of things to do depending on the file present -- A mapping of things to do depending on the file present
local PATH_ACTION_MAP: { [string]: (dir: string) -> number? } = { local PATH_ACTION_MAP: { [string]: (dir: string) -> number? } = {
["default.project.json"] = function(dir) ["default.project.json"] = function(dir)
return process.spawn("rojo", { "sourcemap", dir }, { return process.spawn("rojo", { "sourcemap", dir }, {
cwd = process.cwd, cwd = process.cwd,
env = process.env, env = process.env,
stdio = "forward", stdio = "forward",
}).code }).code
end, end,
["init.lua"] = function() ["init.lua"] = function()
return stdio.write(serde.encode("json", { filePaths = { "init.lua" } }, false)) return stdio.write(
end, serde.encode("json", { filePaths = { "init.lua" } }, false)
["init.luau"] = function() )
return stdio.write(serde.encode("json", { filePaths = { "init.luau" } }, false)) end,
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 --- Writes a Rojo sourcemap for the project in the provided directory or the current
@ -31,18 +35,18 @@ local PATH_ACTION_MAP: { [string]: (dir: string) -> number? } = {
--- * Failure to spawn `rojo` command --- * Failure to spawn `rojo` command
--- * Any I/O error occurs --- * Any I/O error occurs
return function(packageDirectory: string?): boolean return function(packageDirectory: string?): boolean
packageDirectory = packageDirectory or process.cwd packageDirectory = packageDirectory or process.cwd
-- We go through the action mappings in order of priority and check for the -- 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 -- file predicates, if present, we execute the action and report our status
for path, action in PATH_ACTION_MAP do for path, action in PATH_ACTION_MAP do
if fs.isFile(`{packageDirectory}{PLATFORM_SEP}{path}`) then if fs.isFile(`{packageDirectory}{PLATFORM_SEP}{path}`) then
local status = action(packageDirectory) local status = action(packageDirectory)
return if status ~= nil then status == 0 else true return if status ~= nil then status == 0 else true
end end
end end
-- If we reached so far, that must mean none of the file predicates matched, -- If we reached so far, that must mean none of the file predicates matched,
-- so we return a `false` signifying an error -- so we return a `false` signifying an error
return false return false
end end

View file

@ -9,79 +9,98 @@ local check = frktest.assert.check
local TEST_PROJECTS_DIR = "./test-files/rojo/test-projects" local TEST_PROJECTS_DIR = "./test-files/rojo/test-projects"
local TEST_PROJECT_EXCLUDES = { local TEST_PROJECT_EXCLUDES = {
"json_model", "json_model",
"bad_json_model", "bad_json_model",
"plugins", "plugins",
"legacy-0.5.x-unknown-names", "legacy-0.5.x-unknown-names",
} }
local BUILTIN_PATCHES: { local BUILTIN_PATCHES: {
[string]: { [string]: (...any) -> ...any? }, [string]: { [string]: (...any) -> ...any? },
} = { } = {
["@lune/process"] = { ["@lune/process"] = {
spawn = function(program: string, params: { string }, options: process.SpawnOptions): process.SpawnResult spawn = function(
local patchedOptions: process.SpawnOptions = options or {} program: string,
patchedOptions.stdio = "default" params: { string },
options: process.SpawnOptions
): process.SpawnResult
local patchedOptions: process.SpawnOptions = options or {}
patchedOptions.stdio = "default"
local result = process.spawn(program, params, patchedOptions) local result = process.spawn(program, params, patchedOptions)
-- First we make sure the command exited properly -- First we make sure the command exited properly
assert(result.ok, `Expected \`rojo\` command to not error, got:\n{string.rep(" ", 6)}{result.stderr}`) 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 -- We also make sure that the output JSON was valid
serde.decode("json", result.stdout) serde.decode("json", result.stdout)
return result return result
end, end,
}, },
["@lune/stdio"] = { ["@lune/stdio"] = {
write = function(msg: string) write = function(msg: string)
-- Only make sure output JSON is valid -- Only make sure output JSON is valid
serde.decode("json", msg) serde.decode("json", msg)
end, end,
}, },
} }
local function requireWithPatches(path: string) local function requireWithPatches(path: string)
for builtin, patch in BUILTIN_PATCHES do for builtin, patch in BUILTIN_PATCHES do
if path == builtin then if path == builtin then
return setmetatable(patch, { __index = require(builtin) }) return setmetatable(patch, { __index = require(builtin) })
end end
end end
return require(path) return require(path)
end end
return function(test: typeof(frktest.test)) return function(test: typeof(frktest.test))
test.suite("Generates Rojo sourcemaps for test projects successfully", function() test.suite(
for _, file in fs.readDir(TEST_PROJECTS_DIR) do "Generates Rojo sourcemaps for test projects successfully",
if table.find(TEST_PROJECT_EXCLUDES, file) then function()
-- It does not make sense to test sourcemap generation for some of the test projects for _, file in fs.readDir(TEST_PROJECTS_DIR) do
continue if table.find(TEST_PROJECT_EXCLUDES, file) then
end -- It does not make sense to test sourcemap generation for some of the test projects
continue
end
local testProject = `{TEST_PROJECTS_DIR}/{file}` local testProject = `{TEST_PROJECTS_DIR}/{file}`
test.case(file, function() test.case(file, function()
-- If a file starts with `bad_` but not `bad_meta_`, we should expect a failure -- 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 :( -- 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 isBadMeta = regex.new(
local expect = if isBadMeta:isMatch(file) then check.should_error else check.should_not_error "^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() expect(function()
-- We override the require which applies some patches to some builtins, mainly preventing -- 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 -- command stdout forwarding and `stdio.write` outputs to stdout in order to not fill
-- test ouput with large amounts of JSON data -- test ouput with large amounts of JSON data
local sourcemap: (string) -> boolean = local sourcemap: (string) -> boolean = luau.load(
luau.load(fs.readFile("./src/generators/rojo/sourcemap.luau"), { fs.readFile("./src/generators/rojo/sourcemap.luau"),
environment = { {
require = requireWithPatches, environment = {
}, require = requireWithPatches,
})() },
}
)()
return check.is_true(sourcemap(testProject)) return check.is_true(sourcemap(testProject))
end) end)
end) end)
end end
end) end
)
end end

View file

@ -3,49 +3,49 @@ local process = require("@lune/process")
local serde = require("@lune/serde") local serde = require("@lune/serde")
export type TreeProperties = { export type TreeProperties = {
Name: never?, Name: never?,
Parent: never?, Parent: never?,
} }
export type TreeBase = { export type TreeBase = {
["$className"]: string?, ["$className"]: string?,
["$ignoreUnknownInstances"]: boolean?, ["$ignoreUnknownInstances"]: boolean?,
["$path"]: string | { optional: string }?, ["$path"]: string | { optional: string }?,
["$properties"]: TreeProperties?, ["$properties"]: TreeProperties?,
} }
export type TreeNormal = TreeBase & { export type TreeNormal = TreeBase & {
[string]: TreeNormal, [string]: TreeNormal,
} & ({ ["$className"]: string } | { } & ({ ["$className"]: string } | {
["$path"]: string | { optional: string }, ["$path"]: string | { optional: string },
}) })
export type TreeService = TreeBase & { export type TreeService = TreeBase & {
[string]: TreeNormal, [string]: TreeNormal,
} }
export type DataModelTree = TreeBase & { export type DataModelTree = TreeBase & {
StarterPlayer: (TreeBase & { StarterPlayer: (TreeBase & {
StarterPlayerScripts: TreeService?, StarterPlayerScripts: TreeService?,
StarterCharacterScripts: TreeService?, StarterCharacterScripts: TreeService?,
[string]: TreeNormal, [string]: TreeNormal,
})?, })?,
[string]: TreeService, [string]: TreeService,
} }
export type Tree = (DataModelTree & { export type Tree = (DataModelTree & {
["$className"]: "DataModel", ["$className"]: "DataModel",
}) | TreeNormal }) | TreeNormal
export type SyncConfig = { export type SyncConfig = {
name: string, name: string,
servePort: number?, servePort: number?,
servePlaceIds: { number }?, servePlaceIds: { number }?,
placeId: number?, placeId: number?,
gameId: number?, gameId: number?,
serveAddress: string?, serveAddress: string?,
globIgnorePaths: { string }?, globIgnorePaths: { string }?,
tree: Tree, tree: Tree,
} }
local PLATFORM_SEP = if process.os == "windows" then "\\" else "/" local PLATFORM_SEP = if process.os == "windows" then "\\" else "/"
@ -57,57 +57,59 @@ local PLATFORM_SEP = if process.os == "windows" then "\\" else "/"
--- * The current process lacks permissions to a file --- * The current process lacks permissions to a file
--- * Any I/O error occurs --- * Any I/O error occurs
return function( return function(
packageDirectory: string?, packageDirectory: string?,
files: { string }, files: { string },
options: { options: {
writeToFile: boolean?, writeToFile: boolean?,
force: boolean?, force: boolean?,
} }
): (boolean, string?) ): (boolean, string?)
packageDirectory = packageDirectory or process.cwd packageDirectory = packageDirectory or process.cwd
local syncConfigPath = `{packageDirectory}{PLATFORM_SEP}default.project.json` local syncConfigPath =
if fs.isFile(syncConfigPath) and not options.force then `{packageDirectory}{PLATFORM_SEP}default.project.json`
return true, nil if fs.isFile(syncConfigPath) and not options.force then
end return true, nil
end
local syncConfigTree = {} :: Tree local syncConfigTree = {} :: Tree
for _, file in files do for _, file in files do
-- Remove the `.lua` or `.luau` file extension from the file name -- Remove the `.lua` or `.luau` file extension from the file name
local name = string.gsub(file, ".luau?$", "") local name = string.gsub(file, ".luau?$", "")
if name == "init" then if name == "init" then
syncConfigTree["$path"] = name syncConfigTree["$path"] = name
continue continue
end end
syncConfigTree[name] = { syncConfigTree[name] = {
["$path"] = file, ["$path"] = file,
} }
end end
-- If there isn't a top level path, we mark the entire thing as a Folder -- If there isn't a top level path, we mark the entire thing as a Folder
if not syncConfigTree["$path"] then if not syncConfigTree["$path"] then
syncConfigTree["$className"] = "Folder" syncConfigTree["$className"] = "Folder"
end end
-- If the config tree does not include pesde's downloaded roblox dependencies -- 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 -- directory, we add it as an optional one for the future, once dependencies
-- are installed -- are installed
if not syncConfigTree["roblox_packages"] then if not syncConfigTree["roblox_packages"] then
syncConfigTree["roblox_packages"] = { syncConfigTree["roblox_packages"] = {
["$path"] = { ["$path"] = {
optional = "roblox_packages", optional = "roblox_packages",
}, },
} }
end end
-- Finally, we serialize the config to a JSON string and optionally write it -- Finally, we serialize the config to a JSON string and optionally write it
-- to the sync config path -- to the sync config path
local serializedConfig = serde.encode("json", { tree = syncConfigTree }, true) local serializedConfig =
if options.writeToFile then serde.encode("json", { tree = syncConfigTree }, true)
fs.writeFile(syncConfigPath, serializedConfig) if options.writeToFile then
end fs.writeFile(syncConfigPath, serializedConfig)
end
return true, serializedConfig return true, serializedConfig
end end

View file

@ -7,27 +7,34 @@ local check = frktest.assert.check
local syncConfig = require("./sync_config") local syncConfig = require("./sync_config")
local TEST_PROJECTS_DIRS = { local TEST_PROJECTS_DIRS = {
"./test-files/rojo/test-projects", "./test-files/rojo/test-projects",
"./test-files/rojo/rojo-test/serve-tests", "./test-files/rojo/rojo-test/serve-tests",
} }
return function(test: typeof(frktest.test)) return function(test: typeof(frktest.test))
test.suite("Generates Rojo valid sync configs", function() test.suite("Generates Rojo valid sync configs", function()
for _, dir in TEST_PROJECTS_DIRS do for _, dir in TEST_PROJECTS_DIRS do
for _, file in fs.readDir(dir) do for _, file in fs.readDir(dir) do
local fullPath = `{dir}/{file}` local fullPath = `{dir}/{file}`
test.case(`{file}`, function() test.case(`{file}`, function()
local ok, config = syncConfig(fullPath, fs.readDir(fullPath), { writeToFile = false, force = true }) local ok, config = syncConfig(
check.is_true(ok) fullPath,
fs.readDir(fullPath),
{ writeToFile = false, force = true }
)
check.is_true(ok)
-- Make sure that the generated config and the real configs are similar -- Make sure that the generated config and the real configs are similar
local generatedConfig, realConfig = local generatedConfig, realConfig =
serde.decode("json", config), serde.decode("json", config),
serde.decode("json", fs.readFile(`{fullPath}/default.project.json`)) serde.decode(
"json",
fs.readFile(`{fullPath}/default.project.json`)
)
check.table.contains(realConfig, generatedConfig) check.table.contains(realConfig, generatedConfig)
end) end)
end end
end end
end) end)
end end

View file

@ -1,8 +1,8 @@
return { return {
generators = { generators = {
rojo = { rojo = {
sourcemap = require("./generators/rojo/sourcemap"), sourcemap = require("./generators/rojo/sourcemap"),
syncConfig = require("./generators/rojo/sync_config"), syncConfig = require("./generators/rojo/sync_config"),
}, },
}, },
} }