diff --git a/.luaurc b/.luaurc
index 9cc58679..2e9f18f6 100644
--- a/.luaurc
+++ b/.luaurc
@@ -7,5 +7,10 @@
"typeErrors": true,
"globals": [
"warn"
- ]
+ ],
+ "aliases": {
+ "lune": "./types/",
+ "tests": "./tests",
+ "require-tests": "./tests/require/tests"
+ }
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e7ff588e..95c90cf6 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,7 +3,9 @@
"luau-lsp.types.roblox": false,
"luau-lsp.require.mode": "relativeToFile",
"luau-lsp.require.directoryAliases": {
- "@lune/": "./types/"
+ "@lune/": "./types/",
+ "@tests/": "./tests/",
+ "@require-tests/": "./tests/require/tests/"
},
"luau-lsp.ignoreGlobs": [
"tests/roblox/rbx-test-files/**/*.lua",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a55adc9..baf5e077 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,6 +46,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
To compile scripts that use `require` and reference multiple files, a bundler such as [darklua](https://github.com/seaofvoices/darklua) will need to be used first. This limitation will be lifted in the future and Lune will automatically bundle any referenced scripts.
+- Added support for path aliases using `.luaurc` config files!
+
+ For full documentation and reference, check out the [official Luau RFC](https://rfcs.luau-lang.org/require-by-string-aliases.html), but here's a quick example:
+
+ ```jsonc
+ // .luaurc
+ {
+ "aliases": {
+ "modules": "./some/long/path/to/modules"
+ }
+ }
+ ```
+
+ ```lua
+ -- ./some/long/path/to/modules/foo.luau
+ return { World = "World!" }
+
+ -- ./anywhere/you/want/my_script.luau
+ local mod = require("@modules/foo")
+ print("Hello, " .. mod.World)
+ ```
+
- Added support for multiple values for a single query, and multiple values for a single header, in `net.request`. This is a part of the HTTP specification that is not widely used but that may be useful in certain cases. To clarify:
- Single values remain unchanged and will work exactly the same as before.
diff --git a/Cargo.lock b/Cargo.lock
index 1f8c6e2d..e3fbbfe2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1129,6 +1129,7 @@ dependencies = [
"once_cell",
"os_str_bytes",
"path-clean",
+ "pathdiff",
"pin-project",
"rand",
"rbx_binary",
@@ -1390,6 +1391,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
+[[package]]
+name = "pathdiff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+
[[package]]
name = "percent-encoding"
version = "2.3.1"
diff --git a/Cargo.toml b/Cargo.toml
index e7531d63..2e75ce04 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -73,6 +73,7 @@ dialoguer = "0.11"
dunce = "1.0"
lz4_flex = "0.11"
path-clean = "1.0"
+pathdiff = "0.2"
pin-project = "1.0"
urlencoding = "2.1"
diff --git a/src/lune/builtins/process/mod.rs b/src/lune/builtins/process/mod.rs
index 5443a016..15b18335 100644
--- a/src/lune/builtins/process/mod.rs
+++ b/src/lune/builtins/process/mod.rs
@@ -4,12 +4,14 @@ use std::{
process::Stdio,
};
-use dunce::canonicalize;
use mlua::prelude::*;
use os_str_bytes::RawOsString;
use tokio::io::AsyncWriteExt;
-use crate::lune::{scheduler::Scheduler, util::TableBuilder};
+use crate::lune::{
+ scheduler::Scheduler,
+ util::{paths::CWD, TableBuilder},
+};
mod tee_writer;
@@ -26,8 +28,7 @@ yield()
pub fn create(lua: &'static Lua) -> LuaResult {
let cwd_str = {
- let cwd = canonicalize(env::current_dir()?)?;
- let cwd_str = cwd.to_string_lossy().to_string();
+ let cwd_str = CWD.to_string_lossy().to_string();
if !cwd_str.ends_with(path::MAIN_SEPARATOR) {
format!("{cwd_str}{}", path::MAIN_SEPARATOR)
} else {
diff --git a/src/lune/globals/require/alias.rs b/src/lune/globals/require/alias.rs
index f8fd15e1..a4cf1990 100644
--- a/src/lune/globals/require/alias.rs
+++ b/src/lune/globals/require/alias.rs
@@ -1,16 +1,75 @@
+use console::style;
use mlua::prelude::*;
+use crate::lune::util::{
+ luaurc::LuauRc,
+ paths::{make_absolute_and_clean, CWD},
+};
+
use super::context::*;
pub(super) async fn require<'lua, 'ctx>(
- _ctx: &'ctx RequireContext<'lua>,
+ ctx: &'ctx RequireContext<'lua>,
+ source: &str,
alias: &str,
- name: &str,
+ path: &str,
) -> LuaResult>
where
'lua: 'ctx,
{
- Err(LuaError::runtime(format!(
- "TODO: Support require for built-in libraries (tried to require '{name}' with alias '{alias}')"
- )))
+ let alias = alias.to_ascii_lowercase();
+ let path = path.to_ascii_lowercase();
+
+ let parent = make_absolute_and_clean(source)
+ .parent()
+ .expect("how did a root path end up here..")
+ .to_path_buf();
+
+ // Try to gather the first luaurc and / or error we
+ // encounter to display better error messages to users
+ let mut first_luaurc = None;
+ let mut first_error = None;
+ let predicate = |rc: &LuauRc| {
+ if first_luaurc.is_none() {
+ first_luaurc.replace(rc.clone());
+ }
+ if let Err(e) = rc.validate() {
+ if first_error.is_none() {
+ first_error.replace(e);
+ }
+ false
+ } else {
+ rc.find_alias(&alias).is_some()
+ }
+ };
+
+ // Try to find a luaurc that contains the alias we're searching for
+ let luaurc = LuauRc::read_recursive(parent, predicate)
+ .await
+ .ok_or_else(|| {
+ if let Some(error) = first_error {
+ LuaError::runtime(format!("error while parsing .luaurc file: {error}"))
+ } else if let Some(luaurc) = first_luaurc {
+ LuaError::runtime(format!(
+ "failed to find alias '{alias}' - known aliases:\n{}",
+ luaurc
+ .aliases()
+ .iter()
+ .map(|(name, path)| format!(" {name} {} {path}", style(">").dim()))
+ .collect::>()
+ .join("\n")
+ ))
+ } else {
+ LuaError::runtime(format!("failed to find alias '{alias}' (no .luaurc)"))
+ }
+ })?;
+
+ // We now have our aliased path, our path require function just needs it
+ // in a slightly different format with both absolute + relative to cwd
+ let abs_path = luaurc.find_alias(&alias).unwrap().join(path);
+ let rel_path = pathdiff::diff_paths(&abs_path, CWD.as_path()).ok_or_else(|| {
+ LuaError::runtime(format!("failed to find relative path for alias '{alias}'"))
+ })?;
+
+ super::path::require_abs_rel(ctx, abs_path, rel_path).await
}
diff --git a/src/lune/globals/require/context.rs b/src/lune/globals/require/context.rs
index 5f17f002..17522e16 100644
--- a/src/lune/globals/require/context.rs
+++ b/src/lune/globals/require/context.rs
@@ -1,6 +1,5 @@
use std::{
collections::HashMap,
- env,
path::{Path, PathBuf},
sync::Arc,
};
@@ -17,6 +16,7 @@ use tokio::{
use crate::lune::{
builtins::LuneBuiltin,
scheduler::{IntoLuaThread, Scheduler},
+ util::paths::CWD,
};
/**
@@ -28,8 +28,6 @@ use crate::lune::{
#[derive(Debug, Clone)]
pub(super) struct RequireContext<'lua> {
lua: &'lua Lua,
- use_cwd_relative_paths: bool,
- working_directory: PathBuf,
cache_builtins: Arc>>>,
cache_results: Arc>>>,
cache_pending: Arc>>>,
@@ -44,13 +42,8 @@ impl<'lua> RequireContext<'lua> {
than one context may lead to undefined require-behavior.
*/
pub fn new(lua: &'lua Lua) -> Self {
- // FUTURE: We could load some kind of config or env var
- // to check if we should be using cwd-relative paths
- let cwd = env::current_dir().expect("Failed to get current working directory");
Self {
lua,
- use_cwd_relative_paths: false,
- working_directory: cwd,
cache_builtins: Arc::new(AsyncMutex::new(HashMap::new())),
cache_results: Arc::new(AsyncMutex::new(HashMap::new())),
cache_pending: Arc::new(AsyncMutex::new(HashMap::new())),
@@ -70,20 +63,16 @@ impl<'lua> RequireContext<'lua> {
source: impl AsRef,
path: impl AsRef,
) -> LuaResult<(PathBuf, PathBuf)> {
- let path = if self.use_cwd_relative_paths {
- PathBuf::from(path.as_ref())
- } else {
- PathBuf::from(source.as_ref())
- .parent()
- .ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))?
- .join(path.as_ref())
- };
+ let path = PathBuf::from(source.as_ref())
+ .parent()
+ .ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))?
+ .join(path.as_ref());
let rel_path = path_clean::clean(path);
let abs_path = if rel_path.is_absolute() {
rel_path.to_path_buf()
} else {
- self.working_directory.join(&rel_path)
+ CWD.join(&rel_path)
};
Ok((rel_path, abs_path))
diff --git a/src/lune/globals/require/mod.rs b/src/lune/globals/require/mod.rs
index f54ab219..3ba89a06 100644
--- a/src/lune/globals/require/mod.rs
+++ b/src/lune/globals/require/mod.rs
@@ -88,10 +88,10 @@ where
{
builtin::require(&context, &builtin_name).await
} else if let Some(aliased_path) = path.strip_prefix('@') {
- let (alias, name) = aliased_path.split_once('/').ok_or(LuaError::runtime(
+ let (alias, path) = aliased_path.split_once('/').ok_or(LuaError::runtime(
"Require with custom alias must contain '/' delimiter",
))?;
- alias::require(&context, alias, name).await
+ alias::require(&context, &source, alias, path).await
} else {
path::require(&context, &source, &path).await
}
diff --git a/src/lune/globals/require/path.rs b/src/lune/globals/require/path.rs
index 0e443fdc..7333a5b1 100644
--- a/src/lune/globals/require/path.rs
+++ b/src/lune/globals/require/path.rs
@@ -13,7 +13,17 @@ where
'lua: 'ctx,
{
let (abs_path, rel_path) = ctx.resolve_paths(source, path)?;
+ require_abs_rel(ctx, abs_path, rel_path).await
+}
+pub(super) async fn require_abs_rel<'lua, 'ctx>(
+ ctx: &'ctx RequireContext<'lua>,
+ abs_path: PathBuf, // Absolute to filesystem
+ rel_path: PathBuf, // Relative to CWD (for displaying)
+) -> LuaResult>
+where
+ 'lua: 'ctx,
+{
// 1. Try to require the exact path
if let Ok(res) = require_inner(ctx, &abs_path, &rel_path).await {
return Ok(res);
@@ -62,7 +72,7 @@ where
// Nothing left to try, throw an error
Err(LuaError::runtime(format!(
- "No file exist at the path '{}'",
+ "No file exists at the path '{}'",
rel_path.display()
)))
}
diff --git a/src/lune/util/luaurc.rs b/src/lune/util/luaurc.rs
new file mode 100644
index 00000000..69ac64a3
--- /dev/null
+++ b/src/lune/util/luaurc.rs
@@ -0,0 +1,123 @@
+use std::{
+ collections::HashMap,
+ path::{Path, PathBuf, MAIN_SEPARATOR},
+};
+
+use path_clean::PathClean;
+use serde::{Deserialize, Serialize};
+use serde_json::Value as JsonValue;
+use tokio::fs;
+
+use super::paths::make_absolute_and_clean;
+
+const LUAURC_FILE: &str = ".luaurc";
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum LuauLanguageMode {
+ NoCheck,
+ NonStrict,
+ Strict,
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LuauRcConfig {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ language_mode: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ lint: Option>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ lint_errors: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ type_errors: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ globals: Option>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ paths: Option>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ aliases: Option>,
+}
+
+#[derive(Debug, Clone)]
+pub struct LuauRc {
+ dir: PathBuf,
+ config: LuauRcConfig,
+}
+
+impl LuauRc {
+ pub async fn read(dir: impl AsRef) -> Option {
+ let dir = make_absolute_and_clean(dir);
+ let path = dir.join(LUAURC_FILE);
+ let bytes = fs::read(&path).await.ok()?;
+ let config = serde_json::from_slice(&bytes).ok()?;
+ Some(Self { dir, config })
+ }
+
+ pub async fn read_recursive(
+ dir: impl AsRef,
+ mut predicate: impl FnMut(&Self) -> bool,
+ ) -> Option {
+ let mut current = make_absolute_and_clean(dir);
+ loop {
+ if let Some(rc) = Self::read(¤t).await {
+ if predicate(&rc) {
+ return Some(rc);
+ }
+ }
+ if let Some(parent) = current.parent() {
+ current = parent.to_path_buf();
+ } else {
+ return None;
+ }
+ }
+ }
+
+ pub fn validate(&self) -> Result<(), String> {
+ if let Some(aliases) = &self.config.aliases {
+ for alias in aliases.keys() {
+ if !is_valid_alias_key(alias) {
+ return Err(format!("invalid alias key: {}", alias));
+ }
+ }
+ }
+ Ok(())
+ }
+
+ pub fn aliases(&self) -> HashMap {
+ self.config.aliases.clone().unwrap_or_default()
+ }
+
+ pub fn find_alias(&self, name: &str) -> Option {
+ self.config.aliases.as_ref().and_then(|aliases| {
+ aliases.iter().find_map(|(alias, path)| {
+ if alias
+ .trim_end_matches(MAIN_SEPARATOR)
+ .eq_ignore_ascii_case(name)
+ && is_valid_alias_key(alias)
+ {
+ Some(self.dir.join(path).clean())
+ } else {
+ None
+ }
+ })
+ })
+ }
+}
+
+fn is_valid_alias_key(alias: impl AsRef) -> bool {
+ let alias = alias.as_ref();
+ if alias.is_empty()
+ || alias.starts_with('.')
+ || alias.starts_with("..")
+ || alias.chars().any(|c| c == MAIN_SEPARATOR)
+ {
+ false // Paths are not valid alias keys
+ } else {
+ alias.chars().all(is_valid_alias_char)
+ }
+}
+
+fn is_valid_alias_char(c: char) -> bool {
+ c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'
+}
diff --git a/src/lune/util/mod.rs b/src/lune/util/mod.rs
index 3c9457cc..45e75123 100644
--- a/src/lune/util/mod.rs
+++ b/src/lune/util/mod.rs
@@ -2,6 +2,8 @@ mod table_builder;
pub mod formatting;
pub mod futures;
+pub mod luaurc;
+pub mod paths;
pub mod traits;
pub use table_builder::TableBuilder;
diff --git a/src/lune/util/paths.rs b/src/lune/util/paths.rs
new file mode 100644
index 00000000..c439858c
--- /dev/null
+++ b/src/lune/util/paths.rs
@@ -0,0 +1,21 @@
+use std::{
+ env::current_dir,
+ path::{Path, PathBuf},
+};
+
+use once_cell::sync::Lazy;
+use path_clean::PathClean;
+
+pub static CWD: Lazy = Lazy::new(|| {
+ let cwd = current_dir().expect("failed to find current working directory");
+ dunce::canonicalize(cwd).expect("failed to canonicalize current working directory")
+});
+
+pub fn make_absolute_and_clean(path: impl AsRef) -> PathBuf {
+ let path = path.as_ref();
+ if path.is_relative() {
+ CWD.join(path).clean()
+ } else {
+ path.clean()
+ }
+}
diff --git a/src/tests.rs b/src/tests.rs
index eb364fae..8f08cde6 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -82,6 +82,7 @@ create_tests! {
process_spawn_stdin: "process/spawn/stdin",
process_spawn_stdio: "process/spawn/stdio",
+ require_aliases: "require/tests/aliases",
require_async: "require/tests/async",
require_async_background: "require/tests/async_background",
require_async_concurrent: "require/tests/async_concurrent",
diff --git a/tests/require/tests/aliases.luau b/tests/require/tests/aliases.luau
new file mode 100644
index 00000000..09491299
--- /dev/null
+++ b/tests/require/tests/aliases.luau
@@ -0,0 +1,13 @@
+local module = require("@tests/require/tests/module")
+
+assert(type(module) == "table", "Required module did not return a table")
+assert(module.Foo == "Bar", "Required module did not contain correct values")
+assert(module.Hello == "World", "Required module did not contain correct values")
+
+local module2 = require("@require-tests/module")
+
+assert(type(module2) == "table", "Required module did not return a table")
+assert(module2.Foo == "Bar", "Required module did not contain correct values")
+assert(module2.Hello == "World", "Required module did not contain correct values")
+
+assert(module == module2, "Require did not return the same table for the same module")