Skip to Content
PlatformsRobloxPackage Anatomy

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) end

See 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:

  1. The root field in forest.json, if set (a relative path like src/init.luau).
  2. An init.luau or init.lua at the top level of your package directory.
  3. An init.luau or init.lua one directory deep (e.g. src/init.luau).
  4. If none is found, the CLI prompts you for the path and records it as root in forest.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) and forest-lock.json are always excluded when the manifest declares dependencies: consumers regenerate both from your dependency list.
  • Files matched by .gitignore and .forestignore are excluded (.forestignore patterns win).
  • Dotfiles and dot-directories (.git, .DS_Store, and so on) are always excluded.
  • A README.md is required for public packages (shown on the package page).
  • A LICENSE file is required; the CLI detects its SPDX type and records it in forest.json. See Publishing a Package.
  • Packages are capped at 10 MB.
  • Rojo project files (default.project.json) are not used by Forest. The root field, 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.

Last updated on