Log inGet started
·
assettype · drop-in viewer
asset⌬ assettypeassetTypeprimary: type.yaml·originates fromworld 07158574-5…

module.assetType

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 parsers, registries, shared algorithms.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

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 parsers, registries, shared algorithms.

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: the source path with / written as . and each segment's type suffix dropped — /zero/source/demo/beats.module is demo.beats, and /zero/source/kit.package/beats.module is kit.beats, the container's name kept and its .package suffix gone. asset.create returns this string, asset.resolve(...).identity reads it back, and the require forms below write it as <identity>.
  • Folder shape:
    • init.luau (or init.lua) — module body. Required. Returns a table (conventionally named M).
    • README.md — instance-level documentation. Required.
    • .metadata — agent-editable tags + free-form fields. Required.

How to create one

asset.create("module", "<name>")
-- Creates: /zero/source/<name>.module/
--   init.luau   (canonical `local M = {} … return M`)
--   README.md   (instance README template)

-- `folder` places it in a subfolder of /source, and that subfolder
-- leads the identity: `{ folder = "lib" }` produces `lib.<name>`.
-- `into` authors it inside a resolved container (a package, a toolbox),
-- whose own identity leads the same way:
asset.create("module", "<name>", { folder = "lib" })
asset.create("module", "<name>", { into = asset.resolve("myPkg.package") })

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("<identity>") 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. A module is reached by its <identity>, and where the caller lives decides which spelling of it resolves.

    For a module in a world, from a caller in that same world — another module, a component, a scene entrypoint, or an execute chunk:

    • require("<identity>") — the identity on its own, resolved against the caller's own root. It carries every folder segment, so a module created with { folder = "demo" } is require("demo.beats").
    • require("@root::<identity>") — the same root-relative resolution, written out.
    • require(".sibling") — a module in the same folder, by its bare name; ..name steps up one folder, ...name two.

    Logs, stack traces and hot-reload notices name that same module @local.source.<identity> — its key under the world source root. The <identity> tail of that key is the form above, so a trace reading @local.source.demo.beats is require("demo.beats").

    For a module in a library — content under /zero/source/libs/@<lib>/@<lib>:: addresses that library's root from anywhere: require("@builtin::modules.transform"). Mounting a world as a library moves its files under such a root and flips the caller root to @<lib>::, which is what keeps the root-relative forms above resolving across the move.

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.
  • Write exported functions as typed function, not function: the argument types are enforced at the call and the checker knows the signature, so a caller's mistake is reported where they made it.
  • Annotate exported functions with --!desc / --!arg / --!return / --!example so the LSP, tools.list, and the cat summary surface them. A function with no --!desc is reachable only by someone already reading this file.
  • Declare a VALUE with Field.<kind>(default, mode, description), and a table whose members are reached through a metatable with --!members <TypeName> — the latter gives the surface its accepted member set, its type, and its documentation from one table. See guides { path: "core/authoring" } for all four rules and modules/api/engine/entity.module/members.module for a worked member table.
  • 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.
  • Writing one while play runs. A /source write made while play is running lands on the play shadow rather than on disk, and a guarded play-exit discards it. One .module write shadows init.luau, the README.md and the .metadata together. vfs.write(path, bytes, { durable = true }) skips the shadow, and vfs.promotePlayShadow keeps what is already on it — the core/vfs guide has the model and what each route costs.

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.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/asset-type/v1
⌬ Spec
suffix.modulecontainernoprimary aliasesinit.luau, init.luaplural dirmodulesrequired filesinit.luau, README.md, .metadataoptional filesinit.lua
Exposed API
⌬ Instance methods

getInitScript(self: ?) → string

Read the module's entry script as raw text.

argtypedescription
self?

examples

local src = modRef:getInitScript()

getReadme(self: ?) → string

Read the module's README body.

argtypedescription
self?

examples

print(modRef:getReadme())

loadModule(self: ?) → any

Require the module by its canonical identity — same as `require(self.identity)`, but pcall-wrapped so a load failure raises a Luau error tagged with the module identity rather than propagating the raw error.

argtypedescription
self?

examples

local mod = modRef:loadModule()

getExports(self: ?)

The module's exported names and their value types — requires the module and reflects over the table it returns. Available for every module (world-authored or library), since a module is just an asset. the module fails to load or returns a non-table.

argtypedescription
self?

examples

for _, e in ipairs(modRef:getExports() or {}) do print(e.name, e.type) end

inspect(self: ?) → any

`asset.inspect` type-specific detail: `{ exports }`, where `exports` is parsed from the module's own entry script (`getInitScript`) via `luau_introspect.moduleExports` — name/kind/signature/desc of every top-level export, read from the source text rather than a `require()` reflection. Cached on the asset's content checksum, so re-inspecting unchanged source is free. A module with no readable entry script

argtypedescription
self?

examples

local exports = asset.inspect(modRef).detail.exports
⌬ Hooks

onRegister(self: ?) → void

Initial-registration callback: register every `.luau` / `.lua` file this module owns — its entry AND its plain-file submodules — into the `require()` layer, so `require("<mod>.<sub>")` resolves the instant it first registers (fired before the world entrypoint runs). A `.luau` inside a NESTED typed-asset folder (a nested `.module` / `.component` / …) belongs to that asset and registers through its own `onRegister`, so it is skipped here.

argtypedescription
self?The per-instance `AssetRef<module>`.

examples

-- driven by the assetType lifecycle; not called directly

onChange(ref: ?, change: ?) → void

Asset-type change callback: hot-reload this module whenever a `.luau` / `.lua` file inside it is edited (or the module is seeded). This is what live-reloads USER modules — library modules reload through the VFS write hook (author-immutable content does not dispatch `onChange`). Mirrors the `.material` / `.shader` assetTypes owning their own reload. Convergent: the reload only invalidates the require() cache + fires watchers and never writes back into the asset folder.

argtypedescription
ref?
change?

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

3items
This part has no composite children. See the Files segment for its leaf payloads.
backing path · assetTypes/module.assetType

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability
did it work as advertised
quality
authoring polish + cohesion
performance
frame & memory budget held
agent review score
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.