mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 04:50:36 +00:00
Implement support for path aliases in require
This commit is contained in:
parent
0392c163a0
commit
b07202a64b
15 changed files with 287 additions and 31 deletions
7
.luaurc
7
.luaurc
|
@ -7,5 +7,10 @@
|
|||
"typeErrors": true,
|
||||
"globals": [
|
||||
"warn"
|
||||
]
|
||||
],
|
||||
"aliases": {
|
||||
"lune": "./types/",
|
||||
"tests": "./tests",
|
||||
"require-tests": "./tests/require/tests"
|
||||
}
|
||||
}
|
||||
|
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -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",
|
||||
|
|
22
CHANGELOG.md
22
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. <br/>
|
||||
|
|
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
123
src/lune/util/luaurc.rs
Normal 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(¤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<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 == '.'
|
||||
}
|
|
@ -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
21
src/lune/util/paths.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
13
tests/require/tests/aliases.luau
Normal file
13
tests/require/tests/aliases.luau
Normal 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")
|
Loading…
Reference in a new issue