Skip to content

Commit d7c3419

Browse files
committed
Refactor CLI file resolution to be more in line with rbs + use std path instead of str
1 parent fe303b0 commit d7c3419

File tree

3 files changed

+139
-150
lines changed

3 files changed

+139
-150
lines changed

crates/lune-utils/src/path/luau.rs

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,22 @@ impl AsRef<Path> for LuauFilePath {
106106
}
107107
}
108108

109+
impl fmt::Display for LuauFilePath {
110+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111+
match self {
112+
Self::Directory(path) | Self::File(path) => path.display().fmt(f),
113+
}
114+
}
115+
}
116+
109117
/**
110-
A resolved module path for Luau, containing both the source module path,
111-
and the corresponding target path on the current filesystem.
118+
A resolved module path for Luau, containing both:
119+
120+
- The **source** Luau module path.
121+
- The **target** filesystem path.
122+
123+
Note the separation here - the source is not necessarily a valid filesystem path,
124+
and the target is not necessarily a valid Luau module path for require-by-string.
112125
113126
See [`LuauFilePath`] and [`LuauModulePath::resolve`] for more information.
114127
*/
@@ -121,6 +134,38 @@ pub struct LuauModulePath {
121134
}
122135

123136
impl LuauModulePath {
137+
/**
138+
Strips Luau file extensions and potential init segments from a given path.
139+
140+
This is the opposite operation of [`LuauModulePath::resolve`] and is generally
141+
useful for converting between paths in a CLI or other similar use cases - but
142+
should *never* be used to implement `require` resolution.
143+
144+
Does not use any filesystem calls and will not panic.
145+
*/
146+
#[must_use]
147+
pub fn strip(path: impl Into<PathBuf>) -> PathBuf {
148+
let mut path: PathBuf = path.into();
149+
150+
if path
151+
.extension()
152+
.and_then(|e| e.to_str())
153+
.is_some_and(|e| FILE_EXTENSIONS.contains(&e))
154+
{
155+
path = path.with_extension("");
156+
}
157+
158+
if path
159+
.file_name()
160+
.and_then(|e| e.to_str())
161+
.is_some_and(|f| f == FILE_NAME_INIT)
162+
{
163+
path.pop();
164+
}
165+
166+
path
167+
}
168+
124169
/**
125170
Resolves an existing file or directory path for the given *module* path.
126171
@@ -146,30 +191,24 @@ impl LuauModulePath {
146191
}
147192

148193
/**
149-
Returns the source module path.
194+
Returns the source Luau module path.
150195
*/
151196
#[must_use]
152197
pub fn source(&self) -> &Path {
153198
&self.source
154199
}
155200

156201
/**
157-
Returns the target file path.
202+
Returns the target filesystem file path.
158203
*/
159204
#[must_use]
160205
pub fn target(&self) -> &LuauFilePath {
161206
&self.target
162207
}
163208
}
164209

165-
impl AsRef<Path> for LuauModulePath {
166-
fn as_ref(&self) -> &Path {
167-
self.target.as_ref()
168-
}
169-
}
170-
171210
impl fmt::Display for LuauModulePath {
172211
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173-
self.as_ref().display().fmt(f)
212+
self.source().display().fmt(f)
174213
}
175214
}

crates/lune/src/cli/utils/files.rs

Lines changed: 47 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,54 @@
1-
use std::{
2-
fs::Metadata,
3-
path::{MAIN_SEPARATOR, PathBuf},
4-
sync::LazyLock,
5-
};
1+
use std::path::{Path, PathBuf};
62

7-
use anyhow::{Result, anyhow, bail};
3+
use anyhow::{Context, Result, anyhow};
84
use console::style;
95
use directories::UserDirs;
106

11-
const LUNE_COMMENT_PREFIX: &str = "-->";
7+
use lune_utils::path::{LuauFilePath, LuauModulePath, get_current_dir};
128

13-
static ERR_MESSAGE_HELP_NOTE: LazyLock<String> = LazyLock::new(|| {
14-
format!(
15-
"To run this file, either:\n{}\n{}",
16-
format_args!(
17-
"{} rename it to use a {} or {} extension",
18-
style("-").dim(),
19-
style(".luau").blue(),
20-
style(".lua").blue()
21-
),
22-
format_args!(
23-
"{} pass it as an absolute path instead of relative",
24-
style("-").dim()
25-
),
26-
)
27-
});
9+
const LUNE_COMMENT_PREFIX: &str = "-->";
2810

2911
/**
30-
Discovers a script file path based on a given script name.
31-
32-
Script discovery is done in several steps here for the best possible user experience:
33-
34-
1. If we got a file that definitely exists, make sure it is either
35-
- using an absolute path
36-
- has the lua or luau extension
37-
2. If we got a directory, check if it has an `init` file to use, and if it doesn't, let the user know
38-
3. If we got an absolute path, don't check any extensions, just let the user know it didn't exist
39-
4. If we got a relative path with no extension, also look for a file with a lua or luau extension
40-
5. No other options left, the file simply did not exist
12+
Discovers a script file path based on a given module path *or* file path.
4113
42-
This behavior ensures that users can do pretty much whatever they want if they pass in an absolute
43-
path, and that they then have control over script discovery behavior, whereas if they pass in
44-
a relative path we will instead try to be as permissive as possible for user-friendliness
14+
See the documentation for [`LuauModulePath`] for more information about
15+
what a module path vs a script path is.
4516
*/
46-
pub fn discover_script_path(path: impl AsRef<str>, in_home_dir: bool) -> Result<PathBuf> {
47-
// NOTE: We don't actually support any platforms without home directories,
48-
// but just in case the user has some strange configuration and it cannot
49-
// be found we should at least throw a nice error instead of panicking
50-
let path = path.as_ref();
51-
let file_path = if in_home_dir {
52-
match UserDirs::new() {
53-
Some(dirs) => dirs.home_dir().join(path),
54-
None => {
55-
bail!(
56-
"No file was found at {}\nThe home directory does not exist",
57-
style(path).yellow()
58-
)
59-
}
60-
}
17+
pub fn discover_script_path(path: impl Into<PathBuf>, in_home_dir: bool) -> Result<PathBuf> {
18+
// First, for legacy compatibility, we will strip any lua/luau file extension,
19+
// and if the entire file stem is simply "init", we will get rid of that too
20+
// This lets users pass "dir/init.luau" and have it resolve to simply "dir",
21+
// which is a valid luau module path, while "dir/init.luau" is not
22+
let path = LuauModulePath::strip(path);
23+
24+
// If we got an absolute path, we should not modify it,
25+
// otherwise we should either resolve against home or cwd
26+
let path = if path.is_absolute() {
27+
path
28+
} else if in_home_dir {
29+
UserDirs::new()
30+
.context("Missing home directory")?
31+
.home_dir()
32+
.join(path)
6133
} else {
62-
PathBuf::from(path)
34+
get_current_dir().join(path)
6335
};
64-
// NOTE: We use metadata directly here to try to
65-
// avoid accessing the file path more than once
66-
let file_meta = file_path.metadata();
67-
let is_file = file_meta.as_ref().is_ok_and(Metadata::is_file);
68-
let is_dir = file_meta.as_ref().is_ok_and(Metadata::is_dir);
69-
let is_abs = file_path.is_absolute();
70-
let ext = file_path.extension();
71-
if is_file {
72-
if is_abs {
73-
Ok(file_path)
74-
} else if let Some(ext) = file_path.extension() {
75-
match ext {
76-
e if e == "lua" || e == "luau" => Ok(file_path),
77-
_ => Err(anyhow!(
78-
"A file was found at {} but it uses the '{}' file extension\n{}",
79-
style(file_path.display()).green(),
80-
style(ext.to_string_lossy()).blue(),
81-
*ERR_MESSAGE_HELP_NOTE
82-
)),
83-
}
84-
} else {
85-
Err(anyhow!(
86-
"A file was found at {} but it has no file extension\n{}",
87-
style(file_path.display()).green(),
88-
*ERR_MESSAGE_HELP_NOTE
89-
))
90-
}
91-
} else if is_dir {
92-
match (
93-
discover_script_path(format!("{path}/init.luau"), in_home_dir),
94-
discover_script_path(format!("{path}/init.lua"), in_home_dir),
95-
) {
96-
(Ok(path), _) | (_, Ok(path)) => Ok(path),
97-
_ => Err(anyhow!(
98-
"No file was found at {}, found a directory without an init file",
99-
style(file_path.display()).yellow()
36+
37+
// The rest of the logic should follow Luau module path resolution rules
38+
match LuauModulePath::resolve(&path) {
39+
Err(e) => Err(anyhow!(
40+
"Failed to resolve script at path {} ({})",
41+
style(path.display()).yellow(),
42+
style(format!("{e:?}")).red()
43+
)),
44+
Ok(m) => match m.target() {
45+
LuauFilePath::File(f) => Ok(f.clone()),
46+
LuauFilePath::Directory(_) => Err(anyhow!(
47+
"Failed to resolve script at path {}\
48+
\nThe path is a directory without an init file",
49+
style(path.display()).yellow()
10050
)),
101-
}
102-
} else if is_abs && !in_home_dir {
103-
Err(anyhow!(
104-
"No file was found at {}",
105-
style(file_path.display()).yellow()
106-
))
107-
} else if ext.is_none() {
108-
let file_path_lua = file_path.with_extension("lua");
109-
let file_path_luau = file_path.with_extension("luau");
110-
if file_path_lua.is_file() {
111-
Ok(file_path_lua)
112-
} else if file_path_luau.is_file() {
113-
Ok(file_path_luau)
114-
} else {
115-
Err(anyhow!(
116-
"No file was found at {}",
117-
style(file_path.display()).yellow()
118-
))
119-
}
120-
} else {
121-
Err(anyhow!(
122-
"No file was found at {}",
123-
style(file_path.display()).yellow()
124-
))
51+
},
12552
}
12653
}
12754

@@ -134,22 +61,24 @@ pub fn discover_script_path(path: impl AsRef<str>, in_home_dir: bool) -> Result<
13461
13562
Behavior is otherwise exactly the same as for `discover_script_file_path`.
13663
*/
137-
pub fn discover_script_path_including_lune_dirs(path: &str) -> Result<PathBuf> {
64+
pub fn discover_script_path_including_lune_dirs(path: impl AsRef<Path>) -> Result<PathBuf> {
65+
let path: &Path = path.as_ref();
13866
match discover_script_path(path, false) {
13967
Ok(path) => Ok(path),
14068
Err(e) => {
14169
// If we got any absolute path it means the user has also
14270
// told us to not look in any special relative directories
14371
// so we should error right away with the first err message
144-
if PathBuf::from(path).is_absolute() {
72+
if path.is_absolute() {
14573
return Err(e);
14674
}
75+
14776
// Otherwise we take a look in relative lune and .lune
14877
// directories + the home directory for the current user
149-
let res = discover_script_path(format!("lune{MAIN_SEPARATOR}{path}"), false)
150-
.or_else(|_| discover_script_path(format!(".lune{MAIN_SEPARATOR}{path}"), false))
151-
.or_else(|_| discover_script_path(format!("lune{MAIN_SEPARATOR}{path}"), true))
152-
.or_else(|_| discover_script_path(format!(".lune{MAIN_SEPARATOR}{path}"), true));
78+
let res = discover_script_path(Path::new("lune").join(path), false)
79+
.or_else(|_| discover_script_path(Path::new(".lune").join(path), false))
80+
.or_else(|_| discover_script_path(Path::new("lune").join(path), true))
81+
.or_else(|_| discover_script_path(Path::new(".lune").join(path), true));
15382

15483
match res {
15584
// NOTE: The first error message is generally more

crates/lune/src/rt/runtime.rs

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use std::{
1010
};
1111

1212
use async_fs as fs;
13-
use lune_utils::process::{ProcessArgs, ProcessEnv, ProcessJitEnablement};
13+
use lune_utils::{
14+
path::{LuauModulePath, constants::FILE_CHUNK_PREFIX},
15+
process::{ProcessArgs, ProcessEnv, ProcessJitEnablement},
16+
};
1417
use mlua::prelude::*;
1518
use mlua_luau_scheduler::{Functions, Scheduler};
1619

@@ -257,7 +260,9 @@ impl Runtime {
257260
/**
258261
Runs some kind of custom input, inside of the current runtime.
259262
260-
For any input that is a real file path, [`run_file`] should be used instead.
263+
For any input that is a real module or file path, [`run_file`] should
264+
be used instead, since when using this method, any file requires will
265+
fail. Requires for standard libraries and custom libraries will work.
261266
262267
# Errors
263268
@@ -275,7 +280,7 @@ impl Runtime {
275280
}
276281

277282
/**
278-
Runs a file at the given file path, inside of the current runtime.
283+
Runs a file at the given file or module path, inside of the current runtime.
279284
280285
# Errors
281286
@@ -288,16 +293,40 @@ impl Runtime {
288293
&mut self,
289294
path: impl Into<PathBuf>,
290295
) -> RuntimeResult<RuntimeReturnValues> {
291-
let path: PathBuf = path.into();
292-
let contents = fs::read(&path).await.into_lua_err().context(format!(
293-
"Failed to read file at path \"{}\"",
294-
path.display()
295-
))?;
296-
297-
// For calls to `require` to resolve properly, we must convert the file
298-
// path to the respective "module" path according to require-by-string
299-
let module_path = remove_lua_luau_ext(path);
300-
let module_name = format!("@{}", module_path.display());
296+
/*
297+
For calls to `require` to resolve properly, we must:
298+
299+
1. Strip any lua/luau extensions, as well as "init" file
300+
segments from the path.
301+
2. Resolve any given file path to the respective "module"
302+
path according to the require-by-string specification.
303+
304+
After doing this, we should end up with both:
305+
306+
- A source (module path)
307+
- A target (file path)
308+
309+
If the given path was already a valid module path,
310+
this should be a no-op.
311+
*/
312+
let module_or_file_path = LuauModulePath::strip(path);
313+
let module_path = LuauModulePath::resolve(&module_or_file_path)
314+
.map_err(|e| LuaError::external(format!("{e:?}")))
315+
.with_context(|_| {
316+
format!(
317+
"Failed to read file at path \"{}\"",
318+
module_or_file_path.display()
319+
)
320+
})?;
321+
322+
let contents = fs::read(module_path.target())
323+
.await
324+
.into_lua_err()
325+
.with_context(|_| {
326+
format!("Failed to read file at path \"{}\"", module_path.target())
327+
})?;
328+
329+
let module_name = format!("{FILE_CHUNK_PREFIX}{module_path}");
301330
let module_contents = strip_shebang(contents);
302331

303332
self.run_inner(module_name, module_contents).await
@@ -366,14 +395,6 @@ impl Runtime {
366395
}
367396
}
368397

369-
fn remove_lua_luau_ext(path: impl Into<PathBuf>) -> PathBuf {
370-
let path: PathBuf = path.into();
371-
match path.extension().and_then(|e| e.to_str()) {
372-
Some("lua" | "luau") => path.with_extension(""),
373-
_ => path,
374-
}
375-
}
376-
377398
fn strip_shebang(mut contents: Vec<u8>) -> Vec<u8> {
378399
if contents.starts_with(b"#!") {
379400
if let Some(first_newline_idx) = contents

0 commit comments

Comments
 (0)