4.3 KiB
Modules & require
a provides a sandboxed require for splitting a script into multiple files.
It follows stock Lua's conventions — dot-separated module names, one execution
per module, cached results — so editors and the Lua language server understand
the imports without special setup.
local helper = require "lib.helper" -- lib/helper.lua
local db = require "db" -- db.lua or db/init.lua
local mod, path = require "lib.helper" -- second value: the resolved file path
Module names
A name is one or more dot-separated segments; each segment may contain
A–Z a–z 0–9 _ -. Dots map to directory separators, so require "db.migrations"
looks for db/migrations.lua. Paths (/, ..), absolute names, and empty
segments are rejected — a module can only ever be loaded from inside the
search roots, which keeps require compatible with the sandbox: it is the
only file access scripts have.
Search roots
Names are resolved against a fixed list of directories, in order:
- The main script's directory. Symlinks to the script are resolved first,
so a script symlinked into
~/.local/binstill loads modules (and its.env, below) from next to the real file. In the REPL, the current working directory is used instead. - Each entry of
$A_INCLUDE_PATHS, colon-separated like$PATH.
In every root, two candidates are tried (the Lua language server's default templates):
<root>/<name with dots as slashes>.lua
<root>/<name with dots as slashes>/init.lua
The first hit wins. If a later root (or the init.lua form) also matches, a
warning is logged about the shadowed file. When nothing matches, the error
lists every path that was tried, in the stock Lua format.
.env
Before resolving anything, a loads a .env file from the main script's
real directory (the CWD in the REPL), if one exists. Variables already set in
the real environment take precedence. This is the intended place to set
A_INCLUDE_PATHS for a project:
# myproject/.env
A_INCLUDE_PATHS=/home/me/lua-libs:/opt/shared-lua
Since the script's symlink is resolved first, myproject/main.lua symlinked
as ~/.local/bin/mytool still picks up myproject/.env.
Semantics
- A module file is a plain chunk. Its
locals are private to the file; it shares globals with the rest of the program; whatever it returns is the module's value (conventionally a table). Like stock Lua, the chunk receives the module name and the resolved file path as.... - Executed once. The return value is cached by the file's canonical path,
so two names or symlinks reaching the same file share one instance, and
requireof an already-loaded module is just a table lookup. A module that returns nothing is cached astrue. - Return values.
requirereturns the module value and, for file modules, the resolved path as a second value. - Built-ins.
require "http",require "utils", etc. return the same table as the corresponding global, so code written for stock Lua's habits works unchanged. - Cycles. A require cycle (
arequiresbrequiresa) raises acircular requireerror instead of recursing forever. - Errors. If a module raises while loading, the error propagates to the
requiring code and nothing is cached — a later
requireretries the load. - Module files may use the async stdlib (
http,os.sleep, …) at top level, and may start with a shebang and/or UTF-8 BOM, same as the main script.
Editor / LSP setup
The resolution rules match the
Lua language server defaults, so a minimal
.luarc.json next to your project gives go-to-definition and type inference
across require boundaries:
{
"runtime.version": "Lua 5.4",
"runtime.path": ["?.lua", "?/init.lua"],
"workspace.library": ["/home/me/lua-libs", "/opt/shared-lua"]
}
List the same directories in workspace.library as in A_INCLUDE_PATHS;
modules under the script's own directory are found via the workspace root.
Example layout
myproject/
├── main.lua -- #!/usr/bin/env a; symlinked to ~/.local/bin/mytool
├── .env -- A_INCLUDE_PATHS=...
├── .luarc.json
└── lib/
├── helper.lua -- require "lib.helper"
└── db/
└── init.lua -- require "lib.db"