Project Layout
Shape the tree with invisible folders, init folders, metadata and excludes.
Rogen scans the root directories listed in rootDirs and turns what it finds into a tree. A few more rules shape that tree.
Invisible Folders
A folder written as (name) groups files on disk but is left out of the generated tree. The parentheses come off before route keys and tags are matched, so (mock) is a tag folder and (server) a routing folder.
src/Inventory/(Internals)/Slots.luau -> Inventory/SlotsInit Folders
A directory that contains an init or index script is one instance in Rojo. Rogen routes the folder as a whole and doesn't route its contents.
Data Files
Scripts (.luau, .lua, .ts, .tsx), models (.rbxm, .rbxmx) and data files (.json, .toml, .csv, .txt, .yaml, .yml) all become instances, the way Rojo reads them. Route and tag suffixes are read before the extension. TypeScript declaration files (.d.ts) are never scanned.
Metadata Files
A *.meta.json file sets what a file or folder can't express itself: a class, properties or attributes. It is never placed as an instance.
- A file's metadata file is named after the name Rojo gives the file: its name without the extension and without a final
.server,.clientor.plugin.Save.server.luautakesSave.meta.json, and Rojo ignores aSave.server.meta.json.Analytics.mock.luautakesAnalytics.mock.meta.json, even when an active tag renames it toAnalytics: Rojo finds the metadata from the file, not from the name Rogen gives it. Models (.rbxm,.rbxmx,*.model.json) and*.project.jsonfiles take none. - A folder's
init.meta.jsonapplies to every node that folder becomes, which is how a feature folder becomes anActor:
{ "className": "Actor" }With Combat/server/ and Combat/client/, or .server and .client files side by side, Combat becomes one node in each service, and both are Actors. Rojo applies a folder's meta itself where the project points at the folder, so collapsed folders and init folders are left to Rojo. On every other node Rogen copies className, properties, attributes and ignoreUnknownInstances for you, and an id becomes the node's $id. The meta files are JSONC, and fields Rojo doesn't know, such as $schema, are ignored.
When several folders become the same node:
- Across root directories, the last one with an
init.meta.jsonwins, whole. A later folder without one leaves the earlier meta in place. - Within one root directory, two metas reaching one node is an error, for example
src/server/Combat/next tosrc/Combat/. - An
idthat would land on more than one node is an error, since refs must be unique. - Where the template defines the same node, the template wins: its
$propertiesare merged over the meta's key by key, its$attributesreplace the meta's, and its$classNameis kept, with a warning when the two differ.
Some init.meta.json files apply to nothing, and Rogen warns about them:
- One in a routing folder, tag folder or invisible folder, or at the top of a root directory. None of these ever becomes an instance, so put the meta in the feature folder instead.
- One in a folder that shares its instance with a script, such as
Combat/besideCombat.server.luau. Rojo reads the script there, so put the meta inCombat.meta.jsonbeside it, or turn the folder into an init folder.
An invalid init.meta.json stops the build, with the file and position of the problem.
Rogen warns about a *.meta.json that no file beside it claims, and names the file Rojo reads instead.
With a syncDir, Rojo reads the metadata files in your compiler's output. roblox-ts copies them unchanged. Darklua turns each one into a .meta.lua module, so the metadata is lost and Rojo syncs a stray Save.meta ModuleScript. Rogen reads folder meta from the root directories, so the meta it copies survives. For the rest, Rogen warns when a *.meta.json has no copy under the syncDir.
Excluding Files
The exclude field lists globs that are never built, relative to the config's directory. An excluded file is left out of the output.
{ "exclude": ["**/*.spec.luau", "**/*.story.luau"] }Linked Directories
A symlink or Windows junction inside a root directory is read as the directory it points at. That's how code shared between repos gets into one tree. Rogen only ever uses the link's own path, so the generated project file stays portable. A link that loops back to one of its ancestors, or points at nothing, is skipped with a warning.
Several Root Directories
With several root directories, their feature folders merge into one tree. When two of them produce the same instance, the later root directory wins. That's how one codebase serves several places. See the multi-place guide.
Root directories mustn't contain one another. A root directory that doesn't exist is a warning, and watch picks it up when it appears.
Rogen only reads names
The tree is a function of the directory listing, so changing what's inside a script never changes the output.