Log inGet started

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 .module suffix strips from the identity).
  • Folder shape:
    • init.luau (or init.lua) — module body. Required. Returns a table (conventionally named M).
    • 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

  1. Registration. Writing init.luau into a .module/ folder indexes the asset and registers the module path with the resolver.
  2. 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.
  3. Hot reload. Editing the file reloads the module body and invalidates the require cache. Live callers that captured the table via require still hold the old table until they re-require — design for this if you keep state in module-locals.
  4. Identity resolution. Modules can be required by:
    • Fully-qualified identity: require("@<lib>::<path>.<name>").
    • Relative path: require(".sibling") from within the same folder.

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 M table from init.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 / --!example so 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 require chain 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.spawn at module top-level — the engine may not be ready. Expose an M.init and 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.lua vs init.luau. Either works, .luau is canonical.
  • .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.
  • asset-type
  • reference