mirror of
https://github.com/0x5eal/terracotta.git
synced 2024-12-12 04:40:42 +00:00
init + feat: initial darklua backends impl
This commit is contained in:
commit
e14389c538
11 changed files with 526 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Wax related dirs
|
||||
.wax
|
||||
.wax-tmp
|
||||
|
||||
# Compiled outputs
|
||||
terracotta.luau
|
50
.lune/make.luau
Normal file
50
.lune/make.luau
Normal file
|
@ -0,0 +1,50 @@
|
|||
local spawn = require("@lune/process").spawn
|
||||
local stdio = require("@lune/stdio")
|
||||
local fs = require("@lune/fs")
|
||||
|
||||
local codegen = require("../darklua/codegen")
|
||||
|
||||
local WAX_PREFIX_FORMAT = function(type: "error" | "info", scope: string?)
|
||||
return stdio.color(if type == "error" then "red" else "green") .. (scope or "[wax]") .. stdio.color("reset")
|
||||
end
|
||||
|
||||
print(`{WAX_PREFIX_FORMAT("info", "[codegen]")} Generating darklua config file at darklua.config.json`)
|
||||
fs.writeFile(
|
||||
".darklua.json5",
|
||||
codegen({
|
||||
generator = "dense",
|
||||
rules = {
|
||||
"remove_spaces",
|
||||
"rename_variables",
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
print()
|
||||
print("------------------------------------------------------------------")
|
||||
|
||||
codegen({
|
||||
rules = {
|
||||
"config",
|
||||
},
|
||||
})
|
||||
|
||||
local child = spawn(
|
||||
"lune",
|
||||
{ "wax", "bundle", "input=default.project.json", "verbose=true", "minify=true", "output=terracotta.luau" },
|
||||
{
|
||||
stdio = "inherit",
|
||||
}
|
||||
)
|
||||
|
||||
print("------------------------------------------------------------------")
|
||||
|
||||
if not child.ok then
|
||||
print()
|
||||
print(`{WAX_PREFIX_FORMAT("error")} Exited with code {child.code}.`)
|
||||
else
|
||||
print()
|
||||
print(`{WAX_PREFIX_FORMAT("info")} Successfully built terracotta.luau!`)
|
||||
end
|
||||
|
||||
fs.removeFile(".darklua.json5")
|
33
.lune/wax.luau
Normal file
33
.lune/wax.luau
Normal file
|
@ -0,0 +1,33 @@
|
|||
--[[
|
||||
Wax - A Fast Runtime Lua 5.1x+/Luau Project Bundler, Using Roblox Models and Module-Require Semantics
|
||||
MIT License | Copyright (c) 2023 Latte Softworks <https://latte.to>
|
||||
]]
|
||||
|
||||
-- You set the following string to "latest" (case insensitive), or any version tag
|
||||
-- on Wax's releases page (e.g. "0.1.1")
|
||||
local WaxVersion = "latest"
|
||||
local WaxPath = ".wax/wax-${version}.luau"
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local net = require("@lune/net")
|
||||
local luau = require("@lune/luau")
|
||||
local fs = require("@lune/fs")
|
||||
|
||||
local FilePath
|
||||
|
||||
if not fs.isFile(WaxPath) then
|
||||
if not fs.isDir(".wax") then
|
||||
fs.writeDir(".wax")
|
||||
end
|
||||
|
||||
FilePath = WaxPath:gsub("${version}", WaxVersion)
|
||||
|
||||
local FileLink = if string.lower(WaxVersion) == "latest"
|
||||
then "https://github.com/latte-soft/wax/releases/latest/download/wax.luau"
|
||||
else `https://github.com/latte-soft/wax/releases/download/{WaxVersion}/wax.luau`
|
||||
|
||||
fs.writeFile(FilePath, net.request(FileLink).body)
|
||||
end
|
||||
|
||||
luau.load(fs.readFile(FilePath))()
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"luau-lsp.require.mode": "relativeToFile",
|
||||
"luau-lsp.require.directoryAliases": {
|
||||
"@lune/": "~/.lune/.typedefs/0.7.7/"
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
64
darklua/bundler.luau
Normal file
64
darklua/bundler.luau
Normal file
|
@ -0,0 +1,64 @@
|
|||
local Fs = require("@lune/fs")
|
||||
|
||||
local Darklua = require("cmd")
|
||||
local FsUtils = require("../utils/fs")
|
||||
local Codegen = require("codegen")
|
||||
|
||||
local Bundler = {}
|
||||
type BundlerExtended =
|
||||
typeof(Bundler.Prototype)
|
||||
& { darkluaPath: string?, darklua: Darklua.Darklua?, config: { contents: string, path: string? } }
|
||||
|
||||
Bundler.Prototype = {}
|
||||
Bundler.Interface = {}
|
||||
Bundler.Type = "Bundler"
|
||||
|
||||
function Bundler.Prototype.Bundle(
|
||||
self: BundlerExtended,
|
||||
sourceKind: "code" | "path",
|
||||
sourceInner: string
|
||||
): { error: string?, bundled: string? }
|
||||
self.darklua = Darklua.new(self.darkluaPath)
|
||||
|
||||
if self.darklua:IsOk() then
|
||||
return self.darklua:Process("code", sourceInner, self.config.path or (function()
|
||||
local _, path = FsUtils.MakeTemp()
|
||||
Fs.writeFile(
|
||||
path,
|
||||
Codegen({
|
||||
generator = self.config.contents.generator,
|
||||
rules = self.config.contents.rules,
|
||||
})
|
||||
)
|
||||
|
||||
return path
|
||||
end)())
|
||||
end
|
||||
|
||||
return {
|
||||
error = "constructed darklua instance was not OK",
|
||||
}
|
||||
end
|
||||
|
||||
function Bundler.Prototype.ToString(): string
|
||||
return string.format("%s<%s>", Bundler.Type, "{}")
|
||||
end
|
||||
|
||||
function Bundler.Interface.new(darkluaPath: string?, opts: Codegen.Options)
|
||||
return setmetatable({
|
||||
darkluaPath = darkluaPath,
|
||||
darklua = nil,
|
||||
config = {
|
||||
contents = opts,
|
||||
path = nil,
|
||||
},
|
||||
}, {
|
||||
__index = Bundler.Prototype,
|
||||
__type = Bundler.Type,
|
||||
__tostring = function()
|
||||
return Bundler.Prototype.ToString()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return Bundler.Interface
|
203
darklua/cmd.luau
Normal file
203
darklua/cmd.luau
Normal file
|
@ -0,0 +1,203 @@
|
|||
--!strict
|
||||
local Process = require("@lune/process")
|
||||
local Fs = require("@lune/fs")
|
||||
|
||||
local FsUtils = require("../utils/fs")
|
||||
|
||||
local Darklua = {}
|
||||
type DarkluaExtended = typeof(Darklua.Prototype) & { darkluaPath: string }
|
||||
|
||||
-- TODO: Implement wrappers around fs & process APIs that do not panic
|
||||
|
||||
-- TODO: binbows support :3
|
||||
-- Utility function to look for a binary in the PATH and return its absolute path
|
||||
local function ProcessWhich(binName: string): { path: string?, warnings: { string } | {} }
|
||||
local path: string = Process.env.PATH
|
||||
local binPath: string? = nil
|
||||
local warnings: { string } = {}
|
||||
|
||||
-- If the PATH is set, and is a string, we loop divide the path into dirs
|
||||
if path and typeof(path) == "string" then
|
||||
for _, dir in path:split(":") do
|
||||
-- [1] => whether xpcall was okful
|
||||
-- [2] => collection of files in the dir
|
||||
local dirFiles: { any } = table.pack(xpcall(function()
|
||||
-- We try to read the dir
|
||||
return Fs.readDir(dir)
|
||||
end, function()
|
||||
-- If it fails, we push to warnings, and return an empty table
|
||||
table.insert(warnings, string.format("%s - NOT_FOUND", dir))
|
||||
return {}
|
||||
end))[2] :: { any }
|
||||
|
||||
local binIdx = table.find(dirFiles, binName)
|
||||
|
||||
-- Find the position of the binName in our directory, we strip the
|
||||
-- trailing slash, and set the dirPath in the upper scope.
|
||||
if binIdx then
|
||||
local dirPath: string = (function()
|
||||
if string.sub(dir, -1) == "/" then
|
||||
return dir:sub(1, -1)
|
||||
else
|
||||
return dir
|
||||
end
|
||||
end)()
|
||||
|
||||
binPath = dirPath .. "/" .. dirFiles[binIdx]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
path = binPath,
|
||||
warnings = warnings,
|
||||
}
|
||||
end
|
||||
|
||||
Darklua.Type = "Darklua"
|
||||
|
||||
Darklua.Interface = {}
|
||||
Darklua.Prototype = {}
|
||||
|
||||
function Darklua.Prototype.ToString(self: DarkluaExtended): string
|
||||
return string.format("%s<%s>", Darklua.Type, "Global")
|
||||
end
|
||||
|
||||
function Darklua.Prototype.IsOk(self: DarkluaExtended): boolean
|
||||
if self.darkluaPath and Fs.isFile(self.darkluaPath) then
|
||||
local isExecutable = Process.spawn(self.darkluaPath, { "--help" }, { stdio = "default" }).ok
|
||||
|
||||
return isExecutable
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Darklua.Prototype.Process(
|
||||
self: DarkluaExtended,
|
||||
sourceKind: "code" | "path",
|
||||
source: string,
|
||||
configPath: string?
|
||||
): { error: string?, processed: string? }
|
||||
local sourcePath: string
|
||||
|
||||
if sourceKind == "code" then
|
||||
local ok, tmpFilePath = FsUtils.MakeTemp()
|
||||
|
||||
if not ok then
|
||||
return {
|
||||
error = "failed to create temporary file for darklua minification",
|
||||
}
|
||||
end
|
||||
|
||||
Fs.writeFile(tmpFilePath, source)
|
||||
|
||||
sourcePath = tmpFilePath
|
||||
elseif sourceKind == "path" then
|
||||
sourcePath = source
|
||||
else
|
||||
return {
|
||||
error = "invalid sourceKind provided",
|
||||
}
|
||||
end
|
||||
|
||||
local ok, outFilePath = FsUtils.MakeTemp()
|
||||
|
||||
if not ok then
|
||||
return {
|
||||
error = "failed to create temporary file for darklua minification",
|
||||
}
|
||||
end
|
||||
|
||||
local args = { "--config" }
|
||||
|
||||
if configPath and configPath ~= "" then
|
||||
table.insert(args, configPath)
|
||||
else
|
||||
args = {}
|
||||
end
|
||||
|
||||
local darkluaChild = Process.spawn(self.darkluaPath, { "process", sourcePath, outFilePath, table.unpack(args) })
|
||||
|
||||
if not darkluaChild.ok then
|
||||
return {
|
||||
error = "darkluaChild spawning failed, error:\n" .. darkluaChild.stderr,
|
||||
}
|
||||
end
|
||||
|
||||
local processedContents = Fs.readFile(outFilePath)
|
||||
|
||||
return {
|
||||
processed = processedContents,
|
||||
}
|
||||
end
|
||||
|
||||
function Darklua.Prototype.Minify(
|
||||
self: DarkluaExtended,
|
||||
sourceKind: "code" | "path",
|
||||
source: string
|
||||
): { error: string?, minified: string? }
|
||||
local sourcePath: string
|
||||
|
||||
if sourceKind == "code" then
|
||||
local ok, tmpFilePath = FsUtils.MakeTemp()
|
||||
|
||||
if not ok then
|
||||
return {
|
||||
error = "failed to create temporary file for darklua minification",
|
||||
}
|
||||
end
|
||||
|
||||
Fs.writeFile(tmpFilePath, source)
|
||||
|
||||
sourcePath = tmpFilePath
|
||||
elseif sourceKind == "path" then
|
||||
sourcePath = source
|
||||
else
|
||||
return {
|
||||
error = "invalid sourceKind provided",
|
||||
}
|
||||
end
|
||||
|
||||
local ok, outFilePath = FsUtils.MakeTemp()
|
||||
|
||||
if not ok then
|
||||
return {
|
||||
error = "failed to create temporary file for darklua minification",
|
||||
}
|
||||
end
|
||||
|
||||
local darkluaChild = Process.spawn(self.darkluaPath, { "minify", sourcePath, outFilePath })
|
||||
|
||||
if not darkluaChild.ok then
|
||||
return {
|
||||
error = "darkluaChild spawning failed, error:\n" .. darkluaChild.stderr,
|
||||
}
|
||||
end
|
||||
|
||||
local minifiedContents = Fs.readFile(outFilePath)
|
||||
|
||||
return {
|
||||
minified = minifiedContents,
|
||||
}
|
||||
end
|
||||
|
||||
function Darklua.Interface.new(darkluaPath: string?)
|
||||
local inst = setmetatable({
|
||||
darkluaPath = darkluaPath or ProcessWhich("darklua").path,
|
||||
}, {
|
||||
__index = Darklua.Prototype,
|
||||
__type = Darklua.Type,
|
||||
__tostring = function(self: Darklua)
|
||||
return self:ToString()
|
||||
end,
|
||||
})
|
||||
|
||||
return inst :: typeof(inst)
|
||||
end
|
||||
|
||||
-- Here, we provide a path as to stop the ProcessWhich, which adds aditional
|
||||
-- processing we don't need.
|
||||
export type Darklua = typeof(Darklua.Interface.new("NON_EXISTENT_PATH"))
|
||||
|
||||
return Darklua.Interface
|
76
darklua/codegen.luau
Normal file
76
darklua/codegen.luau
Normal file
|
@ -0,0 +1,76 @@
|
|||
local Utils = require("../utils/misc")
|
||||
local JSONEncode = require("@lune/net").jsonEncode
|
||||
|
||||
export type Generators = "retain_lines" | "dense" | "readable"
|
||||
export type Rules =
|
||||
"compute_expression"
|
||||
| "convert_index_to_field"
|
||||
| "convert_local_function_to_assign"
|
||||
| "convert_require"
|
||||
| "filter_after_early_return"
|
||||
| "group_local_assignment"
|
||||
| "inject_global_value"
|
||||
| "remove_comments"
|
||||
| "remove_compound_assignment"
|
||||
| "remove_empty_do"
|
||||
| "remove_function_call_parens"
|
||||
| "remove_method_definition"
|
||||
| "remove_nil_declaration"
|
||||
| "remove_spaces"
|
||||
| "remove_unused_if_branch"
|
||||
| "remove_unused_while"
|
||||
| "rename_variables"
|
||||
|
||||
export type Options = {
|
||||
generator: Generators?,
|
||||
excludes: { string }?,
|
||||
requireMode: "path"?,
|
||||
moduleFolderName: string?,
|
||||
sources: { [string]: string }?,
|
||||
rules: {
|
||||
Rules | {
|
||||
rule: Rules,
|
||||
identifier: string,
|
||||
value: any,
|
||||
} | {
|
||||
rule: Rules,
|
||||
[any]: any,
|
||||
}
|
||||
}?,
|
||||
}
|
||||
|
||||
return function(opts: Options): string
|
||||
local generator: Generators = opts.generator or "retain_lines"
|
||||
local encodedExcludes = JSONEncode(opts.excludes)
|
||||
local excludes = (encodedExcludes == "null" and "[]") or encodedExcludes
|
||||
local requireMode = opts.requireMode or "path"
|
||||
local moduleFolderName = opts.moduleFolderName or "init"
|
||||
local sources = JSONEncode(opts.sources or { ["@terracotta"] = "./.terracotta" })
|
||||
local encodedRules = JSONEncode(opts.rules)
|
||||
local rules = (encodedRules == "null" and "[]") or encodedRules
|
||||
|
||||
local template = [[{
|
||||
generator: "${generator}",
|
||||
|
||||
bundle: {
|
||||
modules_identifier: "__TERRACOTTA__",
|
||||
excludes: ${excludes},
|
||||
|
||||
require_mode: {
|
||||
name: "${requireMode}",
|
||||
module_folder_name: "${moduleFolderName}",
|
||||
sources: ${sources}
|
||||
}
|
||||
},
|
||||
rules: ${rules}
|
||||
}]]
|
||||
|
||||
return Utils.Format(template, {
|
||||
generator = generator,
|
||||
excludes = excludes,
|
||||
requireMode = requireMode,
|
||||
moduleFolderName = moduleFolderName,
|
||||
sources = sources,
|
||||
rules = rules,
|
||||
})
|
||||
end
|
43
darklua/minifier.luau
Normal file
43
darklua/minifier.luau
Normal file
|
@ -0,0 +1,43 @@
|
|||
local Darklua = require("cmd")
|
||||
|
||||
local Minifier = {}
|
||||
type MinfierExtended = typeof(Minifier.Prototype) & { darkluaPath: string?, darklua: Darklua.Darklua? }
|
||||
|
||||
Minifier.Prototype = {}
|
||||
Minifier.Interface = {}
|
||||
Minifier.Type = "Minifier"
|
||||
|
||||
function Minifier.Prototype.Minify(
|
||||
self: MinfierExtended,
|
||||
sourceKind: "code" | "path",
|
||||
sourceInner: string
|
||||
): { error: string?, minified: string? }
|
||||
self.darklua = Darklua.new(self.darkluaPath)
|
||||
|
||||
if self.darklua:IsOk() then
|
||||
return self.darklua:Minify("code", sourceInner)
|
||||
end
|
||||
|
||||
return {
|
||||
error = "constructed darklua instance was not OK",
|
||||
}
|
||||
end
|
||||
|
||||
function Minifier.Prototype.ToString(): string
|
||||
return string.format("%s<%s>", Minifier.Type, "{}")
|
||||
end
|
||||
|
||||
function Minifier.Interface.new(darkluaPath: string?)
|
||||
return setmetatable({
|
||||
darkluaPath = darkluaPath,
|
||||
darklua = nil,
|
||||
}, {
|
||||
__index = Minifier.Prototype,
|
||||
__type = Minifier.Type,
|
||||
__tostring = function()
|
||||
return Minifier.Prototype.ToString()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return Minifier.Interface
|
12
default.project.json
Normal file
12
default.project.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "terracotta",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"terracotta": {
|
||||
"$path": "src/"
|
||||
},
|
||||
"darklua": {
|
||||
"$path": "darklua/"
|
||||
}
|
||||
}
|
||||
}
|
12
utils/fs.luau
Normal file
12
utils/fs.luau
Normal file
|
@ -0,0 +1,12 @@
|
|||
local Process = require("@lune/process")
|
||||
|
||||
local Utils = {}
|
||||
|
||||
-- Utility function to create a temporary directory quickly
|
||||
function Utils.MakeTemp(): (boolean, string)
|
||||
local filePathChild = Process.spawn("mktemp")
|
||||
|
||||
return filePathChild.ok, (filePathChild.ok and filePathChild.stdout:gsub("%s", "")) or ""
|
||||
end
|
||||
|
||||
return Utils
|
20
utils/misc.luau
Normal file
20
utils/misc.luau
Normal file
|
@ -0,0 +1,20 @@
|
|||
local _Net = require("@lune/net")
|
||||
local JSONEncode, JSONDecode = _Net.jsonEncode, _Net.jsonDecode
|
||||
|
||||
local MiscUtils = {}
|
||||
|
||||
function MiscUtils.Format(formatStr: string, with: {[string]: string})
|
||||
return table.pack(string.gsub(formatStr, "\$\{(%a+)\}", function(s)
|
||||
return with[s] or ""
|
||||
end))[1]
|
||||
end
|
||||
|
||||
function MiscUtils.PrettifyJson(json: string | {[any]: any}): string
|
||||
if typeof(json) == "table" then
|
||||
return JSONEncode(json, true)
|
||||
else
|
||||
return JSONEncode(JSONDecode(json), true)
|
||||
end
|
||||
end
|
||||
|
||||
return MiscUtils
|
Loading…
Reference in a new issue