Implement support for path aliases in require

This commit is contained in:
Filip Tibell 2024-01-14 13:33:15 +01:00
parent 0392c163a0
commit b07202a64b
No known key found for this signature in database
15 changed files with 287 additions and 31 deletions

View file

@ -7,5 +7,10 @@
"typeErrors": true,
"globals": [
"warn"
]
],
"aliases": {
"lune": "./types/",
"tests": "./tests",
"require-tests": "./tests/require/tests"
}
}

View file

@ -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",

View file

@ -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. <br/>

7
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<LuaTable> {
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 {

View file

@ -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<LuaMultiValue<'lua>>
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::<Vec<_>>()
.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
}

View file

@ -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<AsyncMutex<HashMap<LuneBuiltin, LuaResult<LuaRegistryKey>>>>,
cache_results: Arc<AsyncMutex<HashMap<PathBuf, LuaResult<LuaRegistryKey>>>>,
cache_pending: Arc<AsyncMutex<HashMap<PathBuf, Sender<()>>>>,
@ -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<str>,
path: impl AsRef<str>,
) -> 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))

View file

@ -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
}

View file

@ -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<LuaMultiValue<'lua>>
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()
)))
}

123
src/lune/util/luaurc.rs Normal file
View file

@ -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<LuauLanguageMode>,
#[serde(skip_serializing_if = "Option::is_none")]
lint: Option<HashMap<String, JsonValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
lint_errors: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
type_errors: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
globals: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
paths: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
aliases: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone)]
pub struct LuauRc {
dir: PathBuf,
config: LuauRcConfig,
}
impl LuauRc {
pub async fn read(dir: impl AsRef<Path>) -> Option<Self> {
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<Path>,
mut predicate: impl FnMut(&Self) -> bool,
) -> Option<Self> {
let mut current = make_absolute_and_clean(dir);
loop {
if let Some(rc) = Self::read(&current).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<String, String> {
self.config.aliases.clone().unwrap_or_default()
}
pub fn find_alias(&self, name: &str) -> Option<PathBuf> {
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<str>) -> 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 == '.'
}

View file

@ -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;

21
src/lune/util/paths.rs Normal file
View file

@ -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<PathBuf> = 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<Path>) -> PathBuf {
let path = path.as_ref();
if path.is_relative() {
CWD.join(path).clean()
} else {
path.clean()
}
}

View file

@ -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",

View file

@ -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")