From ede5bc7d4da376b75f27dfa2143a0d3f88f60377 Mon Sep 17 00:00:00 2001 From: menarulalam <35981995+menarulalam@users.noreply.github.com> Date: Wed, 1 Nov 2023 15:07:53 -0700 Subject: [PATCH] Create require-by-string-aliases.md --- docs/require-by-string-aliases.md | 251 ++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 docs/require-by-string-aliases.md diff --git a/docs/require-by-string-aliases.md b/docs/require-by-string-aliases.md new file mode 100644 index 0000000..b5c9cf1 --- /dev/null +++ b/docs/require-by-string-aliases.md @@ -0,0 +1,251 @@ +# 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 + +#### Aliases + +Aliases can be used to bind an absolute or relative path to a convenient, case-sensitive name that can be required directly. + +```json +"aliases": { + "Roact": "C:/LuauModules/Roact-v1.4.2" +} +``` + +Based on the alias map above, you would be able to require Roact directly: + +```lua +local Roact = require("Roact") +``` + +Or even a sub-module: + +```lua +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. + +##### 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. +- However, the concept of a "Luau library" and its root directory is not yet rigorously defined in Luau, in terms of folder/file structure. +- In the future, we may add a `package.json` file or something similar that marks the root directory of a library, but this is outside of the scope of this RFC, which primarily focuses on improving require-by-string. +- For the time being, this functionality will remain unimplemented for this reason. The alias "`@`" will remain reserved for now, meaning it cannot be overridden. + +Of course, users can still use the alias map to explicitly define this behavior with a named alias: + +```json +"aliases": { + "src": "../" +} +``` + +#### Paths + +Similar to [paths in TypeScript](https://www.typescriptlang.org/tsconfig#paths), 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`): +```json +"paths": [ + "../dependencies", + "/Users/johndoe/MyLuauLibraries", + "/Users/johndoe/MyOtherLuauLibraries", +] +``` + +If `/Users/johndoe/Projects/MyProject/src/init.luau` contained the following code: +```lua +local graphing = require("graphing") +``` +We would search the following directories, in order: +- `/Users/johndoe/Projects/MyProject/src` +- `/Users/johndoe/Projects/MyProject/dependencies` +- `/Users/johndoe/MyLuauLibraries` +- `/Users/johndoe/MyOtherLuauLibraries` + +### 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: +```json +{ + "paths": ["./dependencies"] +} +``` + +Then, `module.luau` could simply require `dependency.luau` like this: +```lua +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. + +### 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 +``` + + +##### Current limitations of aliases + +- Aliases cannot reference other aliases. (However, this is compatible with this proposal and will likely be implemented in the future.) +- Alias names cannot contain the directory separators `/` and `\`. +- Aliases can only occur at the beginning of a path. + +##### 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: +```json +{ + "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 + +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: +- https://www.typescriptlang.org/tsconfig#preserveSymlinks +- https://webpack.js.org/configuration/resolve/#resolvesymlinks + +## 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`: + +```json +{ + "aliases": { + "Roact": "/Users/johndoe/LuauLibraries/Roact/src" + } +} +``` + +Then, we could simply write the following in `module.luau`, and everything would work as intended: +```lua +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`: + +```json +{ + "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`: +```lua +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 `--@ = @`. +```lua +--@"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: +- Could lead to Luau file headers becoming cluttered. +- Would probably lead to substantial copy-and-pasting between modules in the same library, as they would likely need to share certain paths/aliases. +- Using configuration files for paths/alias maps allows modules to share aliases while still providing the flexibility to override if needed. This approach does not support inheritance and overriding in an obvious way. +- Removes the layer of abstraction that is provided by external paths/alias maps. This might also blur the scope of package managers. The software would need to directly modify lines of code in `.luau` files, rather than modifying configuration files. + +#### 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.