Skip to content

Luau Run · Guides · Roblox studio

Requires

In run-server, and in edit without rojo-project, Luau Run builds the test place itself.

For a spec that requires by instance path, such as require(ReplicatedStorage.Shared.Thing), Luau Run needs to know which file that path means. Name the Rojo project that builds your place:

luau-run.toml
[test.roblox-studio]
rojo-project = "test.project.json"

rojo-project is a path to a *.project.json that builds a place. Without it, the lane follows only string requires. luau-run init writes it, and when that project mounts your specs, init also sets data-model = "edit" to run them in your whole place.

Before Studio starts, Rojo writes the project’s sourcemap. Luau Run follows each require through it to a file, and places the file in the test place at the path the project gives it. Only the modules a spec reaches are placed, and only ModuleScripts get source, so nothing in the project starts by itself.

An Actor, Configuration or Model above a placed module keeps its class, so IsA and FindFirstAncestorOfClass work as they do in your game. Other containers above one, such as a Script or a Part, become Folders. Attributes and properties set in Rojo metadata, such as $attributes or a .meta.json, are left out, and a module runs in the runner’s context, not its Actor’s. To keep metadata or Actor context, use data-model = "edit".

The test place keeps three names for Luau Run: ServerStorage._LuauRun, ServerScriptService.TestBootstrap and Workspace.Baseplate. A spec that reaches a module under one of them is refused, and data-model = "edit" runs it.

These forms are followed:

  • game, script, workspace and a service name
  • .Name, ["Name"] and .Parent
  • game:GetService("X"), :WaitForChild("X") and :FindFirstChild("X")
  • locals set to such a path above the require
  • :: T casts

Anything else is refused before Studio starts, naming the file and the require:

  • a computed name
  • FindFirstChild with a second argument
  • a local that is assigned again on a line of its own
  • a path the project does not mount
  • two children of one name
  • something that is not a ModuleScript

Luau Run also refuses:

  • a mounted folder that does not exist, usually packages that are not installed
  • two files Rojo gives one name, such as Thing.lua beside Thing.luau
  • a module under a service the test place does not build, such as Players

Rojo 7.6 cannot write a sourcemap for a project that mounts a path outside its own folder. 7.7.0 can.

Two kinds of require in a module a spec reaches are listed on standard error instead of refused, since they usually do not run:

  • a computed require, such as a lazy require(ModulePath) inside a function
  • a Lune or Lute builtin, such as local task = task or require("@lune/task")

What they would load is not in the test place, so one that does run fails with an error that names it. In a spec itself, or without rojo-project, both are refused.

Locals are followed without scopes, from the nearest local Name = line above the require, so a local hidden by a function parameter can be misread. That can leave a module out of the test place, but never loads a different one: the module fails to load with an error that names it.

An aliased require such as @pkg/Thing is found as Lute finds it: through the nearest .luaurc that declares the alias, from the module’s folder up to the project’s folder. An init.luau starts from the folder above its own. A .config.luau the search reaches is refused, since Luau Run does not run configuration code.

@std and @lute are always Lute’s own modules, whatever a .luaurc declares, so they count as builtins like @lune/task.

Lute skips the .luaurc in a folder that has a module of the same name beside it, such as Thing/ beside Thing.luau. If that .luaurc declares the alias a require needs, the require is refused. Rename the folder or the module.

Your editor can see aliases differently. luau-lsp merges every .luaurc above a module and keeps what it read before an error. Lute and Luau Run use only the nearest one that declares the alias, and stop at an error.

A .json, .jsonc or .toml file the Rojo project mounts is built as Rojo builds it: a ModuleScript that returns the file’s table, reached by instance path or by string require. JSON is read as Rojo 7.6.1 and later read it, with comments, single quotes, bare keys, and trailing or missing commas. Rojo needs 7.4.0 or later for TOML modules, and 7.7.0 or later for .jsonc.

run-server refuses these:

  • a module built from YAML, a model file (.model.json, .rbxm, .rbxmx) or a project’s own syncRules
  • a TOML file with an offset date and time, an integer of 2^53 or more, or a date or time Rojo accepts and Luau Run does not, such as 2023-02-31
  • a JSON or TOML file the Rojo project does not mount, or any data file without rojo-project
  • a ModuleScript with no file of its own: inline in the project file, inside a model file, or made by a meta file
  • a module whose .meta.json or .meta.jsonc sets properties.Source, or whose node in a project file sets $properties.Source, since Rojo builds that Source instead of the file’s

Each refusal points to data-model = "edit" with rojo-project set, where Rojo builds the whole place. A Luau file and a mounted data file of the same name are refused as two modules.

Specs usually load Jest from the project’s own packages, for example require(ReplicatedStorage.Packages["jest_globals"]). On this lane that require returns Jest-Portable’s globals instead of loading the project’s copy. Jest Roblox does the same for any module named JestGlobals. The run says so once on standard error. Recognising the module needs rojo-project.

luau-run.toml
[test.jest-portable]
project-jest-globals = "replace"
project-jest-globals-file = "roblox_packages/jest_globals.luau"

project-jest-globals is "replace" (the default) or "keep", which loads the project’s module like any other. Without project-jest-globals-file, "replace" recognises a module named JestGlobals, or jest-globals inside a jsdotlua_jest-globals@ package folder. With it, only that file is replaced, so a project whose own module is called JestGlobals keeps it. Both settings apply to the Studio lane only.

© 2026 PRMS Developer