Module (asset type)
A module is a reusable, stateless (or globally-stateful) Luau library that other code pulls in with require(). Modules are the unit of factored-out logic in a world: helpers, math libraries, format…
When to use one
- You have logic you want to share across multiple components, services, scenes, or tools.
- You have a registry, lookup table, or shared cache that callers all read from / write to.
- You want a stable API surface (
M.fn) other code can depend on.
If the logic is bound to one entity, use a .component. If you want a
named UI of fields rather than functions, use a .preset.
Where it lives
- Source:
/zero/source/.../<name>.module/ - Identity:
<name>(the.modulesuffix strips from the identity). - Folder shape:
init.luau(orinit.lua) — module body. Required. Returns a table (conventionally namedM).README.md— instance-level documentation. Required.
How to create one
asset.create("module", "<name>")
-- Creates: /zero/source/libs/myLib/<name>.module/
-- init.luau (canonical `local M = {} … return M`)
-- README.md (instance README template)
How it operates
- Registration. Writing
init.luauinto a.module/folder indexes the asset and registers the module path with the resolver. - Loading. The first
require("@scope::path.to.module")runs the file's body and caches the returned table. Subsequent requires from anywhere return the same table — this is the shared-state lever for "module-local" registries. - Hot reload. Editing the file reloads the module body and
invalidates the require cache. Live callers that captured the
table via
requirestill hold the old table until they re-require — design for this if you keep state in module-locals. - Identity resolution. Modules can be required by:
- Fully-qualified identity:
require("@<lib>::<path>.<name>"). - Relative path:
require(".sibling")from within the same folder.
- Fully-qualified identity:
Discovery
asset.list("module")— every registered module.asset.inspect("<name>")— public functions, source path, this type README.cat /zero/source/<name>.module— same summary.
Authoring conventions
- Return a single
Mtable frominit.luau. Top-level statements with side effects run on first require — useful for one-time initialization, dangerous if they touch the engine before it's ready. - Annotate exported functions with
--!desc/--!arg/--!return/--!exampleso the LSP,tools.list, and the cat summary surface them. - Keep module state in module-local upvalues. Globals leak across reloads; module-locals reset cleanly with each hot reload.
- Prefer focused modules over kitchen-sink modules. If two halves of a module have no shared state, split them.
Common pitfalls
- Cyclic requires. A
requirechain that loops will return the partial table (the half built before the cycle was detected). Design for one-way dependencies. - Engine-time side effects. Don't
entity.spawnat module top-level — the engine may not be ready. Expose anM.initand call it from a scene entrypoint. - Hot-reload + module-local cache. A module that caches expensive computation in upvalues loses that cache on reload — fine for dev, but be aware in performance work.
init.luavsinit.luau. Either works,.luauis canonical.
Related types
.component— for entity-bound state + lifecycle hooks..service— generates content you don't have yet (a mesh, sound, texture, …) via a metered provider..tool— for a single agent-callable function with a YAML schema..package— to group several modules + components + scenes into one shippable folder.