lua-forge
v0.1.2
Published
Fast, modular Lua bundler — combine many Lua files into one
Maintainers
Readme
lua-forge
A general-purpose Lua bundler — combine many Lua files into one. Fast, lightweight, easy to maintain. Use it for any Lua runtime: game frameworks, embedded Lua, tooling, or plain standalone scripts.
Why
- Fast builds (content/stat/resolve/parse caching)
- Safe default
runtimemode (handles circular requires) plus opt-inflatfor smaller output - Clear errors: module name + importer + line/column + searched paths
- Usable from both the CLI and the API
- No app-specific logic; no obfuscation/minification by default
Install
npm install lua-forgeWriting Lua modules
Author your code as plain Lua files. A module returns a value (usually a table);
other files pull it in with require("module.name"). Dots map to folders,
exactly like Lua's package.path (require("modules.format") -> modules/format.lua).
src/
├── main.lua -- entry
├── utils.lua
├── shared/config.lua
└── modules/format.lua-- src/modules/format.lua
local format = {}
function format.bold(text)
return "**" .. text .. "**"
end
return format -- the module's return value-- src/utils.lua
local format = require("modules.format") -- resolved & inlined by the bundler
local utils = {}
function utils.greet(name)
return format.bold("hello " .. name)
end
return utils-- src/main.lua (entry — no return needed)
local utils = require("utils")
local config = require("shared.config")
print(utils.greet(config.name))Build it into a single file:
lua-forge build --entry src/main.lua --out build/app.lua --root srcIn the generated bundle, every require("...") that the bundler resolved is
replaced — there is no runtime require left for those. Host-provided globals
(anything that is not a require call) are left untouched.
Run it like any Lua file:
lua build/app.luaModules not bundled (ignored / dynamic)
Some modules you may want to leave out of the bundle (a host-provided library,
a C module, a dynamically-named require). List those names in ignoredModuleNames.
How they are required at runtime depends on target:
target: "generic"(default) — routed to the globalrequire(standard Lua)target: "fivem"(or any host without a globalrequire) — raises a clear error naming the missing module, unless you provide a loader viaruntimeRequire
lua-forge build --entry src/main.lua --out build/app.lua \
--root src --ignore json --require-fn "_G.myloader"Inside the bundle the ignored require("json") becomes __lf_require("json"),
which routes to your runtimeRequire expression (or the global require by default).
CLI
# build (default mode = runtime)
lua-forge build --entry src/main.lua --out build/app.lua
# multi-entry: build several outputs from one config
lua-forge build --config lua-forge.config.ts
# inspect the dependency graph
lua-forge inspect --entry src/main.lua --root .
# benchmark runtime vs flat
lua-forge benchmark --entry src/main.lua --runs 50Main flags: --entry --out --config --mode runtime|flat|auto --target generic|fivem --metadata [true|false|debug] --circular error|runtime-fallback --root --paths --ignore --lua --require-fn --minify --isolate --stats
(--paths / --ignore can be repeated)
API
import { bundle, bundleString, inspect } from "lua-forge";
// bundle from an entry file (writes the file if output is set)
const code = await bundle({
entry: "src/main.lua",
output: "build/app.lua",
mode: "flat",
ignoredModuleNames: ["json"],
});
// bundle from a source string
const out = await bundleString(`local f = require("util")`, { root: "." });
// dependency graph only
const graph = await inspect({ entry: "src/main.lua" });Config
See lua-forge.config.example.ts
| field | default | description |
| --- | --- | --- |
| entry | — | entry file |
| output | — | output path |
| mode | runtime | runtime | flat | auto |
| circular | error | error | runtime-fallback (when flat hits a circular require) |
| entries | — | multi-entry build (several outputs from one config) |
| paths | ["?", "?.lua", "modules/?.lua"] | package.path-style patterns |
| root | entry's dir | base dir for resolution |
| ignoredModuleNames | [] | modules left for the runtime to require itself |
| metadata | false | false (production) | true | "debug" — never leaks an absolute path |
| minify | false | light minify (strips comments/blank lines) |
| isolate | false | do not fall back to the global require |
| luaVersion | 5.4 | 5.4 | 5.3 | LuaJIT |
| target | generic | generic | fivem (a host without a global require) |
| runtimeRequire | — | Lua expression to require modules that are not bundled |
| resolveHook | — | custom resolution |
| dynamicRequireHook | — | handle require(var) |
| persistentCache | false | path to store the parse cache (keyed by content hash) |
Output modes
runtime (default) — uses __bundle_require + module factories + a loaded cache (fast path),
plus localized globals (type/tostring/error); supports circular requires and keeps require
semantics intact.
flat — orders modules dependency-first, each module becomes a local var.
No runtime loader, smaller, faster to load, but opt-in because it rewrites bundled require
calls aggressively. Cannot be used with circular requires (errors, or falls back per circular).
auto — keeps the old heuristic: picks flat when there is no circular require; on a circular
require it follows circular (error = stop and report the cycle, runtime-fallback = switch
to runtime automatically).
Hosts without a global require
Some Lua hosts (for example FiveM's CfxLua) have no global require. lua-forge
builds its own require runtime (__bundle_require in runtime mode / inline vars
in flat mode), so the output does not rely on a global require for bundled
modules.
For such a host set target: "fivem" (or any value other than generic).
Then any non-bundled module raises a clear error instead of crashing on a nil
call — provide runtimeRequire (e.g. "_G.myloader" or "exports.x.require")
if you have your own loader. Point the host at the single bundled file, e.g. in
FiveM:
-- fxmanifest.lua
client_scripts { 'build/client.lua' }
server_scripts { 'build/server.lua' }Compatibility
The output is platform-independent by design:
- Pure ASCII, LF-only output — no machine paths, no BOM, no CRLF leak from sources
- Deterministic — the same sources produce byte-identical output on any OS (Windows/macOS/Linux)
- Any Lua runtime — generated code uses only standard constructs and runs on Lua 5.1–5.4 and LuaJIT
- Standalone — bundled requires need no external loader; non-bundled ones are configurable per host
The CLI/API run on Node >= 18 on any OS. A .ts config additionally needs
Node >= 22.18 (native type stripping); .js / .mjs / .json configs work on any supported Node.
Dev
npm install
npm test
npm run build # -> dist/
npm run typecheck