Require by String with Aliases

Summary

We need to add intuitive alias and paths functionality to facilitate the grouping together of related Luau files into libraries and allow for future package managers to be developed and integrated easily.

Motivation

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 does not support aliases.

Design

Parts of this design are now obsoleted by this RFC.

Aliases

Aliases can be used to bind an absolute or relative path to a convenient, case-insensitive name that can be required directly.

"aliases": {
    "Roact": "C:/LuauModules/Roact-v1.4.2"
}

Based on the alias map above, you would be able to require Roact directly with an @ prefix:

local Roact = require("@Roact")

Or even a sub-module:

local createElement = require("@Roact/createElement")

Aliases are overrides. Whenever the first component of a path exactly matches a pre-defined alias, it will be replaced before the path is resolved to a file. Alias names are also restricted to the charset [A-Za-z0-9.\-_]. We restrict the charset and make them case insensitive because we envision alias names to be primarily used as package names, which tend to be case insensitive and alphanumeric. They also must be preceded by an @ symbol.

Package management

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:

-- Requiring Roact by absolute path
local Roact = require("C:/LuauModules/Roact-v1.4.2")

If we wanted to change the location of this package on disk, we would have to modify all files that require the old path above, which essentially boils down to manual package management. This approach is tedious and prone to mistakes.

To solve this, we introduce path and alias configuration in this RFC, which would allow for future package management software to maintain package paths and versions by directly modifying configuration files.

This would also create simple and readable require statements for developers.

-- Suppose "@src" is an alias for the same directory as "../../../../../"

-- Instead of this:
local result = require("../../../../../HelperModules/graphing")

-- We could have this:
local result = require("@src/HelperModules/graphing")
Versioning

Aliases are simple bindings and aren’t concerned with versioning. The intention is to allow package management software to leverage aliases by automatically adding and updating the alias map to reflect a package’s dependencies. For manual versioning, a user could always “version” their aliases: @MyModule-v1, @MyModule-v2, etc.

Library root alias

In the past, it has been proposed to define an alias (e.g. “@”) to represent the root directory of a file’s encapsulating library.

Of course, users can still use the alias map to explicitly define this behavior with a named alias:

"aliases": {
    "src": "../"
}

Paths

Similar to paths in TypeScript, we will introduce a paths array that can be configured in .luaurc files. Whenever a path is passed to require and does not begin with ./ or ../, the path will first be resolved relative to the requiring file. If this fails, we will attempt to resolve paths relative to each path in the paths array.

The paths array can contain absolute paths, and relative paths are resolved relative to .luaurc file in which they appear.

Example Definition

With the given paths definition (.luaurc file located in /Users/johndoe/Projects/MyProject/src):

"paths": [
    "../dependencies",
    "/Users/johndoe/MyLuauLibraries",
    "/Users/johndoe/MyOtherLuauLibraries",
]

If /Users/johndoe/Projects/MyProject/src/init.luau contained the following code:

local graphing = require("graphing")

We would search the following directories, in order:

Paths array

The paths configuration variable provides convenience and allows Luau developers to build complex, well-organized libraries. Imagine the following project structure:

luau-paths-project
├── .luaurc
├── dependencies
│   └── dependency.luau
└── src
    └── module.luau

If .luaurc contained the following paths array:

{
    "paths": ["./dependencies"]
}

Then, module.luau could simply require dependency.luau like this:

local dependency = require("dependency")

-- Instead of: require("../dependencies/dependency")

Using the paths array allows Luau developers to organize their projects however they like without compromising code readability.

Current limitations of aliases
Configuring alias maps: .luaurc

As part of this proposal, alias maps will be configured in .luaurc, which follows a JSON-like syntax.

Proposed structure of an alias map:

{
    "aliases": {
        "alias1": "/path/of/alias1",
        "alias2": "/path/of/alias2"
    }
}

Missing aliases in .luaurc are inherited from the alias maps of any parent directories, and fields can be overridden.

Additionally, if an alias is bound to a relative path, the path will be evaluated relative to the .luaurc file in which the alias was defined.

Finally, providing support for alias maps within the Roblox engine is out of the scope of this RFC but is being considered internally.

Symlinks carry some security concerns; for example, a link’s target might exist outside of the project folder in which the link was defined. For the first version of this implementation, symlinks will not be supported and will be treated as ordinary files.

If we do implement symlinks in the future, we will likely use our own limit to the number of symlinks to “follow through” for cross-platform compatibility. It is also possible that we will add a new configurable property in .luaurc that will allow developers to toggle whether or not to resolve symlinks.

Similar examples:

Use Cases

Alias map

Using alias maps, Luau developers can require globally installed Luau libraries in their code without needing to specify their locations in Luau scripts.

luau-aliases-project
├── .luaurc
└── src
    └── module.luau

For example, if we wanted to require Roact in module.luau, we could add the following alias to .luaurc:

{
    "aliases": {
        "Roact": "/Users/johndoe/LuauLibraries/Roact/src"
    }
}

Then, we could simply write the following in module.luau, and everything would work as intended:

local Roact = require("@Roact")
local Component = require("@Roact/Component")

If we ever wanted to change the version of Roact used by luau-aliases-project, we would simply change the absolute path to Roact in .luaurc. By abstracting away the exact location of globally installed libraries like this, we get clean, readable code, and we make it easier for a future package manager to update dependencies by modifying .luaurc files.

Large-scale projects in Luau

For large-scale Luau projects, we might imagine that every dependency of the project is a Luau project itself. We might use an organizational structure like this to create a clean hierarchy:

large-luau-project
├── .luaurc
├── subproject-1
├── subproject-2
└── subproject-3

We can provide the following alias in large-luau-project/.luaurc:

{
    "aliases": {
        "com.roblox.luau": "."
    }
}

This way, each subproject directory can contain its own source code, dependencies, and .luaurc configuration files, while also inheriting the com.roblox.luau alias from large-luau-project/.luaurc.

This allows us to refer to other subprojects like this, regardless of the exact location of the requiring file in large-luau-project:

local subproject1 = require("@com.roblox.luau/subproject-1")

Roblox Specifics

In the Roblox engine, a similar aliasing system could be implemented. Assuming a central package management system were available, a Roblox Script could contain local Roact = require(“@Roact”), and everything would “just work”.

Drawbacks

Backwards Compatibility

This alias system introduces a new layer to require that wasn’t previously there. However, the advantages of this system are expected to outweigh the complexities it introduces.

Alternatives

Different ways of defining aliases

Defining paths/aliases directly in the requiring file

Rather than defining paths/alias maps in an external configuration file, we could alternatively define paths/aliases directly in the files that require them. For example, this could manifest itself through an extension of the --! comment syntax or introduce new syntax like --@<ALIAS> = @<PATH>.

--@"Roact" = @"C:/LuauModules/Roact-v1.4.2"
local Roact = require("@Roact")

-- Same as this:
local Roact = require("C:/LuauModules/Roact-v1.4.2")

Some potential issues with this approach:

Defining configuration files in a non-JSON format

Alias maps could alternatively be defined in specially named .luau files themselves, or at least adhering to Luau syntax. This approach is somewhat unappealing, however, as it would require package management software to parse Luau syntax. JSON syntax is well-understood and well-supported, which would likely facilitate the development of package management software.