How It Works
Instead of asking you to tag what is public, LuauDocs discovers your public surface from your code. That single rule decides which pages exist, what they're named, and how the sidebar is organized.
Your module's return value
LuauDocs never executes your code. It parses the AST and evaluates top-level statements symbolically. It follows assignments, requires, type casts, and metatables to determine what your module's return statement produces. Whatever that returned value can reach is your public surface:
local M = {}
function M.visible() end -- documented: reachable from the returned table
local function helper() end -- not documented: nothing exposes it
return MTwo rules apply to return:
- Only top-level
returnstatements count. The evaluator ignoresreturnstatements insideifblocks or inner scopes. - The return value must resolve to a table or a passthrough
require. When it can't (such as a call into another module or an unresolvable conditional), LuauDocs emits thesurface-opaquediagnostic.
Private members
A leading underscore marks a member as private and omits it from the site. Two exceptions stay public regardless of name: export type declarations and double-underscore metamethod functions (__name) that have a doc comment. Undocumented __ functions are omitted.
You can override this per member with @private or @ignore, or include private members across the entire site with [api] includePrivate.
What the extractor understands
Without any tags, the extractor discovers:
- Table-literal members, including doc comments on individual fields.
function M.f,function M:method,M.x = value, and explicit-selfmethodsfunction M.f(self: M, ...)(rendered asM:f(...)).__indexclass idioms,setmetatable(M, { __index = Proto })delegation, andtable.freezewrappers. This includes classes kept on unreturned local tables when attached by a constructor (setmetatable(_, Impl)) or a metatable type (typeof(setmetatable(_, Impl))orsetmetatable<_, typeof(Impl)>). Baresetmetatable<D, M>isn't recognized as a class type.- Cast types (
:: T) and container type annotations. Unannotated or:: anyproperties inherit their field type and doc comment from the owning table's type annotation, andKey: typeof(require(...))fields synthesize module mounts. - Re-exported requires, lazy loader functions (
function M.sub() return require(...) end), and member imports (isState = State.isState). - Conditional mounts (
if IsClient then M.UI = require(...) endandRunService:IsServer() and require(...)). - String requires (
./xand@self/x, which fall back to the Rojo tree when no sibling file matches on disk, plus@alias/xvia.luaurc) and runtime aliases (@lute/fs,@lune/net) configured under[source.aliases]. - Roblox instance requires (
script.Parent.X,:WaitForChild("X")), including Rojo*.project.jsonmounts and nested project files. - Behavioral badges inferred from code for yielding functions, realm-restricted calls (server, client, or plugin), realm-guarded properties, and
table.freezeorread-typed properties. See Adding badges. - Property types inferred from assigned values when unannotated (see Property types come from the value).
- Parameter descriptions from trailing comments in multi-line signatures.
Unresolved requires emit require-unresolved instead of failing silently.
Access paths, not folders
Every surfaced module gets an access path, which is the expression a caller writes to reach it (Flux.Util.Queue), regardless of where the file sits on disk. A module reachable by multiple paths uses the shortest one.
This site's example library renders its access tree directly on the API index:
Queue sits under Util because callers reach it as Flux.Util.Queue, not because its file is examples/Flux/Util/Queue.luau.
The API sidebar
The sidebar mirrors the access tree. It starts with an Overview link to /api/, followed by a section for each entry module that exposes submodules, and ends with a Reference section for standalone modules. A single-module library shows Overview followed by Reference.
Flux.Util.Queue places Queue inside the Util group under Flux, where clicking Util opens the Util page. Breadcrumbs at the top of each page trace the same hierarchy (Home › Overview › Flux › Util on Queue).
The API sidebar takes no configuration. There are no settings to reorder, pin, or rename API sidebar groups because the sidebar always reflects what your code exposes.
Re-exports and internals
Re-exports render inline on the exposing page. A mounted module renders as a property that links to its page and carries a Module badge. A member import (isState = State.isState) renders a full entry under the exposed name with a from State badge linked to its definition.
In Util/init.luau, Util.Queue = Queue produces:
On the root module, Flux.isState carries its from State badge.
If a module has doc comments but isn't exposed on the public surface, LuauDocs files it by project structure instead. It uses the Rojo tree when default.project.json (or [source] projectFile) is present, or the folder layout otherwise.
Types get their own entries
Every export type (and any local type referenced by a documented signature) gets a dedicated entry. Field descriptions come from doc comments or trailing -- comments. Re-exported type aliases (export type T = Other.T) expand the origin type's definition and fields automatically.
--[[ A first-in, first-out queue. ]]
export type Queue<T> = typeof(setmetatable(
{} :: {
items: { T }, -- everything still waiting, front first
},
Queue
))Types
Queue type
type Queue<T> = typeof(setmetatable(
{} :: {
items: { T }, -- everything still waiting, front first
},
Queue
))A first-in, first-out queue.
Type references in signatures link to their declaration. Roblox types (CFrame, Instance, enums) link to Creator Documentation (controlled by [api] linkRobloxTypes). Types reached through a runtime alias (fs.File) link to the runtime's documentation or remote doc model.
Property types come from the value
When a property has no explicit annotation, LuauDocs infers its type from unambiguous right-hand expressions:
Settings.IsClient = RunService:IsClient() -- boolean
Settings.accent = Color3.fromRGB(0, 122, 204) -- Color3
Settings.sortOrder = Enum.SortOrder.LayoutOrder -- Enum.SortOrder
Settings.container = script:WaitForChild("Container") -- Instance
Settings.changed = Signal.new() -- Signal, from what Signal.new returns
Settings.tags = { "ui", "theme" } -- { string }Cross-module calls (Signal.new()) inherit the callee's annotated return type and link to its declaration.
Non-boolean expressions guarded by RunService:IsServer() or RunService:IsClient() get both a realm badge and an optional type:
Store.folder = RunService:IsServer() and script:WaitForChild("Storage")
-- the entry reads: folder [Server], Store.folder: Instance?The guard adds Server (even when guarded through a local variable) and marks the inferred type optional (Instance?). Boolean predicates (IS_PRIVATE = RunService:IsServer() and game.PrivateServerId ~= "") keep their boolean type without a realm badge.
Exposing one table under two names in the same module (Z.boolean = Z.bool) documents the container under the first name and renders the second as a property linked to the first.
Ambiguous expressions (a + b, script.Name) stay untyped unless you annotate the variable, add a :: cast, annotate the owning table type, or write a @prop name Type tag.
Where to go from here
See Writing Doc Comments for adding prose and cross-references, and Overriding with Tags for overriding inferred behavior.