Package Anatomy
This page describes what a Roblox Forest package is: its entry point, its file layout, and what actually ends up in the published archive.
The entry point rule
Every package has exactly one entry point: its root module. Whatever that module returns is the package’s entire public API.
There is no way to expose multiple top-level modules from one package. If your library has several surfaces (a server API and a client API, utilities plus a main class, and so on), the root module must return all of them, typically as a table:
-- init.luau
return {
Server = require(script.Server),
Client = require(script.Client),
}or by branching on context at require time:
-- init.luau
local RunService = game:GetService("RunService")
if RunService:IsServer() then
return require(script.Server)
else
return require(script.Client)
endSee Recipe: Server & Client APIs for a full walkthrough of this pattern.
The root module
forest init asks for your root module’s path when you scaffold the package (default src/init.luau, created as a starter if it doesn’t exist) and records it as root in forest.json.
For manifests without a root (packages created before init recorded it), forest publish resolves the root module in this order:
- The
rootfield inforest.json, if set (a relative path likesrc/init.luau). - An
init.luauorinit.luaat the top level of your package directory. - An
init.luauorinit.luaone directory deep (e.g.src/init.luau). - If none is found, the CLI prompts you for the path and records it as
rootinforest.json.
The root file does not have to be named init. You can declare any module file as the root (e.g. root: "src/MyLibrary.luau"). On install, Forest renames it to init.luau/init.lua so the installed package folder is always directly requirable in Roblox.
What gets published
The published archive is re-rooted at the directory containing the root module. Everything in that directory (the root file, its sibling modules, and nested folders) becomes the installed package. Your top-level LICENSE file is included alongside it.
- forest.json
- README.md
- LICENSE
- init.luau
- Helper.luau
installs for a consumer as:
- init.luau
- Helper.luau
- LICENSE
Rules applied when the archive is built:
- The
Packages/install mount (next to your root module) andforest-lock.jsonare always excluded when the manifest declares dependencies: consumers regenerate both from your dependency list. - Files matched by
.gitignoreand.forestignoreare excluded (.forestignorepatterns win). - Dotfiles and dot-directories (
.git,.DS_Store, and so on) are always excluded. - A
README.mdis required for public packages (shown on the package page). - A
LICENSEfile is required; the CLI detects its SPDX type and records it inforest.json. See Publishing a Package. - Packages are capped at 10 MB.
- Rojo project files (
default.project.json) are not used by Forest. Therootfield, not a Rojo tree, defines the package.
Developing with dependencies
Inside a package under development, forest install places dependencies in a Packages/ folder next to your root module (src/Packages/ in the default layout):
- forest.json
- forest-lock.json
- init.luau
That is exactly where dependencies sit when a consumer installs your package (inside the package’s own folder), so requires like require(script.Packages.signal) behave identically in your workspace and in every consumer’s tree. As noted above, the mount and forest-lock.json never ship in the archive.
The folder is named Packages by default. forest init --packages-dir <name> picks a different name, recorded as packagesDir in forest.json; the name travels with each published version, so consumers recreate your package’s dependency folder under the same name.
Sibling modules are reachable, not private
Everything inside the package folder ships to consumers. Modules that aren’t re-exported by the root are conventionally internal, but they still exist as instances in the consumer’s datamodel and their source is visible wherever the packages tree is mounted (see Where packages live). Don’t put secrets in a package, and don’t rely on “not exported” as a security boundary.