From e14389c5384d23a96d09c625734b6e200e6f5ceb Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 26 Sep 2023 19:14:58 +0530 Subject: [PATCH] init + feat: initial darklua backends impl --- .gitignore | 6 ++ .lune/make.luau | 50 +++++++++++ .lune/wax.luau | 33 +++++++ .vscode/settings.json | 7 ++ darklua/bundler.luau | 64 +++++++++++++ darklua/cmd.luau | 203 ++++++++++++++++++++++++++++++++++++++++++ darklua/codegen.luau | 76 ++++++++++++++++ darklua/minifier.luau | 43 +++++++++ default.project.json | 12 +++ utils/fs.luau | 12 +++ utils/misc.luau | 20 +++++ 11 files changed, 526 insertions(+) create mode 100644 .gitignore create mode 100644 .lune/make.luau create mode 100644 .lune/wax.luau create mode 100644 .vscode/settings.json create mode 100644 darklua/bundler.luau create mode 100644 darklua/cmd.luau create mode 100644 darklua/codegen.luau create mode 100644 darklua/minifier.luau create mode 100644 default.project.json create mode 100644 utils/fs.luau create mode 100644 utils/misc.luau diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e2d3df --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Wax related dirs +.wax +.wax-tmp + +# Compiled outputs +terracotta.luau \ No newline at end of file diff --git a/.lune/make.luau b/.lune/make.luau new file mode 100644 index 0000000..d53a406 --- /dev/null +++ b/.lune/make.luau @@ -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") diff --git a/.lune/wax.luau b/.lune/wax.luau new file mode 100644 index 0000000..d63f84d --- /dev/null +++ b/.lune/wax.luau @@ -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 +]] + +-- 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))() diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a0f94ce --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "luau-lsp.require.mode": "relativeToFile", + "luau-lsp.require.directoryAliases": { + "@lune/": "~/.lune/.typedefs/0.7.7/" + }, + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/darklua/bundler.luau b/darklua/bundler.luau new file mode 100644 index 0000000..97d5f6b --- /dev/null +++ b/darklua/bundler.luau @@ -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 diff --git a/darklua/cmd.luau b/darklua/cmd.luau new file mode 100644 index 0000000..ec4f668 --- /dev/null +++ b/darklua/cmd.luau @@ -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 diff --git a/darklua/codegen.luau b/darklua/codegen.luau new file mode 100644 index 0000000..9af2a0a --- /dev/null +++ b/darklua/codegen.luau @@ -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 diff --git a/darklua/minifier.luau b/darklua/minifier.luau new file mode 100644 index 0000000..c52713a --- /dev/null +++ b/darklua/minifier.luau @@ -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 diff --git a/default.project.json b/default.project.json new file mode 100644 index 0000000..aaf4e9e --- /dev/null +++ b/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "terracotta", + "tree": { + "$className": "Folder", + "terracotta": { + "$path": "src/" + }, + "darklua": { + "$path": "darklua/" + } + } +} \ No newline at end of file diff --git a/utils/fs.luau b/utils/fs.luau new file mode 100644 index 0000000..3e81ece --- /dev/null +++ b/utils/fs.luau @@ -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 \ No newline at end of file diff --git a/utils/misc.luau b/utils/misc.luau new file mode 100644 index 0000000..a7738b8 --- /dev/null +++ b/utils/misc.luau @@ -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 \ No newline at end of file