@@ -14,7 +14,7 @@ use crate::state::{callback_error_ext, Lua};
1414use crate :: table:: Table ;
1515use crate :: types:: MaybeSend ;
1616
17- /// An error that can occur during navigation in the Luau `require` system.
17+ /// An error that can occur during navigation in the Luau `require-by-string ` system.
1818#[ cfg( any( feature = "luau" , doc) ) ]
1919#[ cfg_attr( docsrs, doc( cfg( feature = "luau" ) ) ) ]
2020#[ derive( Debug , Clone ) ]
@@ -50,7 +50,7 @@ impl From<Error> for NavigateError {
5050#[ cfg( feature = "luau" ) ]
5151type WriteResult = ffi:: luarequire_WriteResult ;
5252
53- /// A trait for handling modules loading and navigation in the Luau `require` system.
53+ /// A trait for handling modules loading and navigation in the Luau `require-by-string ` system.
5454#[ cfg( any( feature = "luau" , doc) ) ]
5555#[ cfg_attr( docsrs, doc( cfg( feature = "luau" ) ) ) ]
5656pub trait Require : MaybeSend {
@@ -103,16 +103,26 @@ impl fmt::Debug for dyn Require {
103103 }
104104}
105105
106- /// The standard implementation of Luau `require` navigation.
107- #[ doc( hidden) ]
106+ /// The standard implementation of Luau `require-by-string` navigation.
108107#[ derive( Default , Debug ) ]
109108pub struct TextRequirer {
109+ /// An absolute path to the current Luau module (not mapped to a physical file)
110110 abs_path : PathBuf ,
111+ /// A relative path to the current Luau module (not mapped to a physical file)
111112 rel_path : PathBuf ,
112- module_path : PathBuf ,
113+ /// A physical path to the current Luau module, which is a file or a directory with an
114+ /// `init.lua(u)` file
115+ resolved_path : Option < PathBuf > ,
113116}
114117
115118impl TextRequirer {
119+ /// The prefix used for chunk names in the require system.
120+ /// Only chunk names starting with this prefix are allowed to be used in `require`.
121+ const CHUNK_PREFIX : & str = "@" ;
122+
123+ /// The file extensions that are considered valid for Luau modules.
124+ const FILE_EXTENSIONS : & [ & str ] = & [ "luau" , "lua" ] ;
125+
116126 /// Creates a new `TextRequirer` instance.
117127 pub fn new ( ) -> Self {
118128 Self :: default ( )
@@ -156,44 +166,48 @@ impl TextRequirer {
156166 components. into_iter ( ) . collect ( )
157167 }
158168
159- fn find_module ( path : & Path ) -> StdResult < PathBuf , NavigateError > {
169+ /// Resolve a Luau module path to a physical file or directory.
170+ ///
171+ /// Empty directories without init files are considered valid as "intermediate" directories.
172+ fn resolve_module ( path : & Path ) -> StdResult < Option < PathBuf > , NavigateError > {
160173 let mut found_path = None ;
161174
162175 if path. components ( ) . next_back ( ) != Some ( Component :: Normal ( "init" . as_ref ( ) ) ) {
163176 let current_ext = ( path. extension ( ) . and_then ( |s| s. to_str ( ) ) )
164177 . map ( |s| format ! ( "{s}." ) )
165178 . unwrap_or_default ( ) ;
166- for ext in [ "luau" , "lua" ] {
179+ for ext in Self :: FILE_EXTENSIONS {
167180 let candidate = path. with_extension ( format ! ( "{current_ext}{ext}" ) ) ;
168181 if candidate. is_file ( ) && found_path. replace ( candidate) . is_some ( ) {
169182 return Err ( NavigateError :: Ambiguous ) ;
170183 }
171184 }
172185 }
173186 if path. is_dir ( ) {
174- for component in [ "init.luau" , "init.lua" ] {
187+ for component in Self :: FILE_EXTENSIONS . iter ( ) . map ( |ext| format ! ( "init.{ext}" ) ) {
175188 let candidate = path. join ( component) ;
176189 if candidate. is_file ( ) && found_path. replace ( candidate) . is_some ( ) {
177190 return Err ( NavigateError :: Ambiguous ) ;
178191 }
179192 }
180193
181194 if found_path. is_none ( ) {
182- found_path = Some ( PathBuf :: new ( ) ) ;
195+ // Directories without init files are considered valid "intermediate" path
196+ return Ok ( None ) ;
183197 }
184198 }
185199
186- found_path. ok_or ( NavigateError :: NotFound )
200+ Ok ( Some ( found_path. ok_or ( NavigateError :: NotFound ) ? ) )
187201 }
188202}
189203
190204impl Require for TextRequirer {
191205 fn is_require_allowed ( & self , chunk_name : & str ) -> bool {
192- chunk_name. starts_with ( '@' )
206+ chunk_name. starts_with ( Self :: CHUNK_PREFIX )
193207 }
194208
195209 fn reset ( & mut self , chunk_name : & str ) -> StdResult < ( ) , NavigateError > {
196- if !chunk_name. starts_with ( '@' ) {
210+ if !chunk_name. starts_with ( Self :: CHUNK_PREFIX ) {
197211 return Err ( NavigateError :: NotFound ) ;
198212 }
199213 let chunk_name = Self :: normalize_chunk_name ( & chunk_name[ 1 ..] ) ;
@@ -205,74 +219,79 @@ impl Require for TextRequirer {
205219 let cwd = env:: current_dir ( ) . map_err ( |_| NavigateError :: NotFound ) ?;
206220 self . abs_path = Self :: normalize_path ( & cwd. join ( chunk_filename) ) ;
207221 self . rel_path = ( [ Component :: CurDir , Component :: Normal ( chunk_filename) ] . into_iter ( ) ) . collect ( ) ;
208- self . module_path = PathBuf :: new ( ) ;
222+ self . resolved_path = None ;
209223
210224 return Ok ( ( ) ) ;
211225 }
212226
213227 if chunk_path. is_absolute ( ) {
214- let module_path = Self :: find_module ( & chunk_path) ?;
228+ let resolved_path = Self :: resolve_module ( & chunk_path) ?;
215229 self . abs_path = chunk_path. clone ( ) ;
216230 self . rel_path = chunk_path;
217- self . module_path = module_path ;
231+ self . resolved_path = resolved_path ;
218232 } else {
219233 // Relative path
220234 let cwd = env:: current_dir ( ) . map_err ( |_| NavigateError :: NotFound ) ?;
221235 let abs_path = Self :: normalize_path ( & cwd. join ( & chunk_path) ) ;
222- let module_path = Self :: find_module ( & abs_path) ?;
236+ let resolved_path = Self :: resolve_module ( & abs_path) ?;
223237 self . abs_path = abs_path;
224238 self . rel_path = chunk_path;
225- self . module_path = module_path ;
239+ self . resolved_path = resolved_path ;
226240 }
227241
228242 Ok ( ( ) )
229243 }
230244
231245 fn jump_to_alias ( & mut self , path : & str ) -> StdResult < ( ) , NavigateError > {
232246 let path = Self :: normalize_path ( path. as_ref ( ) ) ;
233- let module_path = Self :: find_module ( & path) ?;
247+ let resolved_path = Self :: resolve_module ( & path) ?;
234248
235249 self . abs_path = path. clone ( ) ;
236250 self . rel_path = path;
237- self . module_path = module_path ;
251+ self . resolved_path = resolved_path ;
238252
239253 Ok ( ( ) )
240254 }
241255
242256 fn to_parent ( & mut self ) -> StdResult < ( ) , NavigateError > {
243257 let mut abs_path = self . abs_path . clone ( ) ;
244258 if !abs_path. pop ( ) {
259+ // It's important to return `NotFound` if we reached the root, as it's a "recoverable" error if we
260+ // cannot go beyond the root directory.
261+ // Luau "require-by-string` has a special logic to search for config file to resolve aliases.
245262 return Err ( NavigateError :: NotFound ) ;
246263 }
247264 let mut rel_parent = self . rel_path . clone ( ) ;
248265 rel_parent. pop ( ) ;
249- let module_path = Self :: find_module ( & abs_path) ?;
266+ let resolved_path = Self :: resolve_module ( & abs_path) ?;
250267
251268 self . abs_path = abs_path;
252269 self . rel_path = Self :: normalize_path ( & rel_parent) ;
253- self . module_path = module_path ;
270+ self . resolved_path = resolved_path ;
254271
255272 Ok ( ( ) )
256273 }
257274
258275 fn to_child ( & mut self , name : & str ) -> StdResult < ( ) , NavigateError > {
259276 let abs_path = self . abs_path . join ( name) ;
260277 let rel_path = self . rel_path . join ( name) ;
261- let module_path = Self :: find_module ( & abs_path) ?;
278+ let resolved_path = Self :: resolve_module ( & abs_path) ?;
262279
263280 self . abs_path = abs_path;
264281 self . rel_path = rel_path;
265- self . module_path = module_path ;
282+ self . resolved_path = resolved_path ;
266283
267284 Ok ( ( ) )
268285 }
269286
270287 fn has_module ( & self ) -> bool {
271- self . module_path . is_file ( )
288+ ( self . resolved_path . as_deref ( ) )
289+ . map ( Path :: is_file)
290+ . unwrap_or ( false )
272291 }
273292
274293 fn cache_key ( & self ) -> String {
275- self . module_path . display ( ) . to_string ( )
294+ self . resolved_path . as_deref ( ) . unwrap ( ) . display ( ) . to_string ( )
276295 }
277296
278297 fn has_config ( & self ) -> bool {
@@ -285,7 +304,9 @@ impl Require for TextRequirer {
285304
286305 fn loader ( & self , lua : & Lua ) -> Result < Function > {
287306 let name = format ! ( "@{}" , self . rel_path. display( ) ) ;
288- lua. load ( & * self . module_path ) . set_name ( name) . into_function ( )
307+ lua. load ( self . resolved_path . as_deref ( ) . unwrap ( ) )
308+ . set_name ( name)
309+ . into_function ( )
289310 }
290311}
291312
@@ -496,7 +517,7 @@ unsafe fn write_to_buffer(
496517}
497518
498519#[ cfg( feature = "luau" ) ]
499- pub fn create_require_function < R : Require + ' static > ( lua : & Lua , require : R ) -> Result < Function > {
520+ pub ( super ) fn create_require_function < R : Require + ' static > ( lua : & Lua , require : R ) -> Result < Function > {
500521 unsafe extern "C-unwind" fn find_current_file ( state : * mut ffi:: lua_State ) -> c_int {
501522 let mut ar: ffi:: lua_Debug = mem:: zeroed ( ) ;
502523 for level in 2 .. {
0 commit comments