We need to add relative paths to require
statements to facilitate the grouping together of related Luau files into libraries and allow for future package managers to be developed and integrated easily.
The Roblox engine does not currently support require-by-string. One motivation for this RFC is to consolidate require syntax and functionality between the Roblox engine and the broader Luau language itself.
Luau itself currently supports a basic require-by-string syntax that allows for requiring Luau modules by relative or absolute path. Unfortunately, the current implementation has a few issues.
Currently, relative paths are always evaluated relative to the current working directory that the Luau CLI is running from. This leads to unexpected behavior when requiring modules from “incorrect” working directories.
Suppose the module math.luau
is located in /Users/JohnDoe/LuauModules/Math
and contains the following:
-- Beginning of /Users/JohnDoe/LuauModules/Math/math.luau
local sqrt = require("../MathHelperFunctions/sqrt")
If we then launched the Luau CLI from the directory /Users/JohnDoe/Projects/MyCalculator
and required math.luau
as follows:
local math = require("/Users/JohnDoe/LuauModules/Math/math")
This would cause the following:
math.luau
, as its absolute path was given.math.luau
from the context of the current working directory /Users/JohnDoe/Projects/MyCalculator
.sqrt.luau
from math.luau
, instead of looking in the directory /Users/JohnDoe/LuauModules/MathHelperFunctions
, the relative path will be evaluated in relation to the current working directory.sqrt.luau
in /Users/JohnDoe/Projects/MathHelperFunctions
, which is a directory that may not even exist.This behavior is problematic, and puts an unnecessary emphasis on the directory from which the Luau CLI is running. A better solution is to evaluate relative paths in relation to the file that is requiring them.
While package management itself is outside of the scope of this RFC, we want to make it possible for a package management solution to be developed in a way that integrates well with our require syntax.
To require a Luau module under the current implementation, we must require it either by relative or absolute path:
Our require syntax should:
For compatibility across platforms, we will automatically map /
onto \
.
If we find files with the same name but different extensions, then we will attempt to require a file with the following extensions (in this order):
.luau
.lua
If the string resolves to a directory instead of a file, then we will attempt to require a file in that directory with the following name (in this order):
init.luau
init.lua
Modules can be required relative to the requiring file’s location in the filesystem (note, this is different from the current implementation, which evaluates all relative paths in relation to the current working directory).
If we are trying to require a module called MyModule.luau
in C:/MyLibrary
:
local MyModule = require("MyModule")
-- From C:/MyLibrary/SubDirectory/SubModule.luau
local MyModule = require("../MyModule")
-- From C:/MyOtherLibrary/MainModule.luau
local MyModule = require("../MyLibrary/MyModule")
Relative paths may begin with ./
or ../
, which denote the directory of the requiring file and its parent directory, respectively. When a relative path does begin with one of these prefixes, it will only be resolved relative to the requiring file. If these prefixes are not provided, path resolution will fallback to checking the paths in the paths
configuration variable, as described later.
When a require statement is executed directly in a REPL input prompt (not in a file), relative paths will be evaluated in relation to the pseudo-file stdin
, located in the current working directory. If the code being executed is not tied to a file (e.g. using loadstring
), executing any require statements in this code will result in an error.
Absolute paths will no longer be supported in require
statements, as they are unportable. The only way to require by absolute path will be through a explicitly defined paths or aliases defined in configuration files, as described in another RFC.
The current implementation of evaluating relative paths (in relation to the current working directory) can be found in lua_require.
When reading in the contents of a module using readFile, the function fopen
/_wfopen
is called, which itself evaluates relative paths in relation to the CWD. In order to implement relative paths in relation to the requiring file, we have two options when evaluating a relative path.
Assume the following:
"/Users/johndoe/project/subdirectory/cwd"
"/Users/johndoe/project/requirer.luau"
"./sibling"
Approach 1: Translate to the “correct” relative path
fopen
/_wfopen
: "../../sibling"
Approach 2: Convert the given relative path into its corresponding absolute path
fopen
/_wfopen
: "/Users/johndoe/project/sibling"
Although fopen
/_wfopen
can handle both relative (to CWD) and absolute paths, the second approach makes more sense for our use case. We already need absolute paths for caching, as explained in the “Caching” section, so we might as well generate these absolute paths during the path resolution stage. With the first approach, we would need to call realpath
to convert the relative-to-CWD path into an absolute path for caching, which is an unnecessary extra step.
However, for security reasons, we don’t want to expose absolute paths directly to Luau scripts (for example, through debug.info
). To prevent this, even though we will cache and read files by absolute path (which helps reduce cache misses), the chunkname
used to load code here will be the file’s path relative to the current working directory. This way, the output of debug.info
will be unaffected by this RFC’s proposed changes.
chunkname
would be "../../requirer.luau"
, and its cache key would be "/Users/johndoe/project/requirer.luau"
(was set when it was required)."./sibling"
, we would apply this to "../../requirer.luau"
, obtaining the relative-to-cwd path "../../sibling.luau"
.chunkname
of sibling.luau
during loading."/Users/johndoe/project/sibling"
for caching by resolving it relative to the CWD.In the case of an aliased path, it doesn’t make sense to make the path relative to the CWD. In this case, the alias would remain in the chunkname
to prevent leaking any absolute paths.
One key assumption of this section is that we will have the absolute path of the requiring file when requiring a module by relative path.
While we could add an explicit reference to this directory to the lua_State
, we already have an internal mechanism that allows us to get this information. We essentially want to call the C++
equivalent of debug.info(1, "s")
when we enter lua_require
, which would return the name of the file that called require
, or stdin
if the module was required directly from the CLI.
As an example, we might do something like this in lua_require
:
static int lua_require(lua_State* L)
{
lua_Debug ar;
lua_getinfo(L, 1, "s", &ar);
// Path of requiring file
const char* basepath = ar.source;
// ...
}
debug.info
outputThe current implementation also has a slight inconsistency that should be addressed. When executing a Luau script directly (launching Luau with a command-line argument: "luau script.luau"
), that base script’s name is internally stored with a file extension. However, files that are later required are stored with this extension omitted. As a result, the output of debug.info
depends on whether the file was the base Luau script being executed or was required as a dependency of the base script.
For consistency, we propose storing the file extension in lua_require
and always outputting it when debug.info
is called.
By interpreting relative paths relative to the requiring file’s location, Luau projects can now have internal dependencies. For example, in Roact’s current implementation, Component.lua
requires assign.lua
like this:
local assign = require(script.Parent.assign)
By using “Roblox-style” syntax (referring to Roblox Instances in the require statement), Component.lua
is able to perform a relative-to-requiring-script require. However, with the proposed changes in this RFC, we could instead do this with clean syntax that works outside of the context of Roblox:
local assign = require("./assign")
(Of course, for this to work in the Roblox engine, there needs to be support for require-by-string in the engine. This is being discussed internally.)
Luau libraries are already not compatible with existing Lua libraries. This is because Lua favors the .
based require syntax instead and relies on the LUA_PATH
environment variable to search for modules, whereas Luau currently supports a basic require-by-string syntax.
stdin
, located in the current working directory.loadstring
), executing require statements contained in this code will throw an error.@CWD
).In considering alternatives to enhancing relative imports in Luau, one can draw inspiration from other language systems. An elegant solution is the package import system similar to Dart’s approach. Instead of relying on file-specific paths, this proposed system would utilize an absolute package:
syntax:
import 'package:my_package/my_file.lua';
Undesirable because this would be redundant with the alias RFC.