@@ -14,7 +14,7 @@ use crate::state::{callback_error_ext, Lua};
14
14
use crate :: table:: Table ;
15
15
use crate :: types:: MaybeSend ;
16
16
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.
18
18
#[ cfg( any( feature = "luau" , doc) ) ]
19
19
#[ cfg_attr( docsrs, doc( cfg( feature = "luau" ) ) ) ]
20
20
#[ derive( Debug , Clone ) ]
@@ -50,7 +50,7 @@ impl From<Error> for NavigateError {
50
50
#[ cfg( feature = "luau" ) ]
51
51
type WriteResult = ffi:: luarequire_WriteResult ;
52
52
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.
54
54
#[ cfg( any( feature = "luau" , doc) ) ]
55
55
#[ cfg_attr( docsrs, doc( cfg( feature = "luau" ) ) ) ]
56
56
pub trait Require : MaybeSend {
@@ -103,16 +103,26 @@ impl fmt::Debug for dyn Require {
103
103
}
104
104
}
105
105
106
- /// The standard implementation of Luau `require` navigation.
107
- #[ doc( hidden) ]
106
+ /// The standard implementation of Luau `require-by-string` navigation.
108
107
#[ derive( Default , Debug ) ]
109
108
pub struct TextRequirer {
109
+ /// An absolute path to the current Luau module (not mapped to a physical file)
110
110
abs_path : PathBuf ,
111
+ /// A relative path to the current Luau module (not mapped to a physical file)
111
112
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 > ,
113
116
}
114
117
115
118
impl 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
+
116
126
/// Creates a new `TextRequirer` instance.
117
127
pub fn new ( ) -> Self {
118
128
Self :: default ( )
@@ -156,44 +166,48 @@ impl TextRequirer {
156
166
components. into_iter ( ) . collect ( )
157
167
}
158
168
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 > {
160
173
let mut found_path = None ;
161
174
162
175
if path. components ( ) . next_back ( ) != Some ( Component :: Normal ( "init" . as_ref ( ) ) ) {
163
176
let current_ext = ( path. extension ( ) . and_then ( |s| s. to_str ( ) ) )
164
177
. map ( |s| format ! ( "{s}." ) )
165
178
. unwrap_or_default ( ) ;
166
- for ext in [ "luau" , "lua" ] {
179
+ for ext in Self :: FILE_EXTENSIONS {
167
180
let candidate = path. with_extension ( format ! ( "{current_ext}{ext}" ) ) ;
168
181
if candidate. is_file ( ) && found_path. replace ( candidate) . is_some ( ) {
169
182
return Err ( NavigateError :: Ambiguous ) ;
170
183
}
171
184
}
172
185
}
173
186
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}" ) ) {
175
188
let candidate = path. join ( component) ;
176
189
if candidate. is_file ( ) && found_path. replace ( candidate) . is_some ( ) {
177
190
return Err ( NavigateError :: Ambiguous ) ;
178
191
}
179
192
}
180
193
181
194
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 ) ;
183
197
}
184
198
}
185
199
186
- found_path. ok_or ( NavigateError :: NotFound )
200
+ Ok ( Some ( found_path. ok_or ( NavigateError :: NotFound ) ? ) )
187
201
}
188
202
}
189
203
190
204
impl Require for TextRequirer {
191
205
fn is_require_allowed ( & self , chunk_name : & str ) -> bool {
192
- chunk_name. starts_with ( '@' )
206
+ chunk_name. starts_with ( Self :: CHUNK_PREFIX )
193
207
}
194
208
195
209
fn reset ( & mut self , chunk_name : & str ) -> StdResult < ( ) , NavigateError > {
196
- if !chunk_name. starts_with ( '@' ) {
210
+ if !chunk_name. starts_with ( Self :: CHUNK_PREFIX ) {
197
211
return Err ( NavigateError :: NotFound ) ;
198
212
}
199
213
let chunk_name = Self :: normalize_chunk_name ( & chunk_name[ 1 ..] ) ;
@@ -205,74 +219,79 @@ impl Require for TextRequirer {
205
219
let cwd = env:: current_dir ( ) . map_err ( |_| NavigateError :: NotFound ) ?;
206
220
self . abs_path = Self :: normalize_path ( & cwd. join ( chunk_filename) ) ;
207
221
self . rel_path = ( [ Component :: CurDir , Component :: Normal ( chunk_filename) ] . into_iter ( ) ) . collect ( ) ;
208
- self . module_path = PathBuf :: new ( ) ;
222
+ self . resolved_path = None ;
209
223
210
224
return Ok ( ( ) ) ;
211
225
}
212
226
213
227
if chunk_path. is_absolute ( ) {
214
- let module_path = Self :: find_module ( & chunk_path) ?;
228
+ let resolved_path = Self :: resolve_module ( & chunk_path) ?;
215
229
self . abs_path = chunk_path. clone ( ) ;
216
230
self . rel_path = chunk_path;
217
- self . module_path = module_path ;
231
+ self . resolved_path = resolved_path ;
218
232
} else {
219
233
// Relative path
220
234
let cwd = env:: current_dir ( ) . map_err ( |_| NavigateError :: NotFound ) ?;
221
235
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) ?;
223
237
self . abs_path = abs_path;
224
238
self . rel_path = chunk_path;
225
- self . module_path = module_path ;
239
+ self . resolved_path = resolved_path ;
226
240
}
227
241
228
242
Ok ( ( ) )
229
243
}
230
244
231
245
fn jump_to_alias ( & mut self , path : & str ) -> StdResult < ( ) , NavigateError > {
232
246
let path = Self :: normalize_path ( path. as_ref ( ) ) ;
233
- let module_path = Self :: find_module ( & path) ?;
247
+ let resolved_path = Self :: resolve_module ( & path) ?;
234
248
235
249
self . abs_path = path. clone ( ) ;
236
250
self . rel_path = path;
237
- self . module_path = module_path ;
251
+ self . resolved_path = resolved_path ;
238
252
239
253
Ok ( ( ) )
240
254
}
241
255
242
256
fn to_parent ( & mut self ) -> StdResult < ( ) , NavigateError > {
243
257
let mut abs_path = self . abs_path . clone ( ) ;
244
258
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.
245
262
return Err ( NavigateError :: NotFound ) ;
246
263
}
247
264
let mut rel_parent = self . rel_path . clone ( ) ;
248
265
rel_parent. pop ( ) ;
249
- let module_path = Self :: find_module ( & abs_path) ?;
266
+ let resolved_path = Self :: resolve_module ( & abs_path) ?;
250
267
251
268
self . abs_path = abs_path;
252
269
self . rel_path = Self :: normalize_path ( & rel_parent) ;
253
- self . module_path = module_path ;
270
+ self . resolved_path = resolved_path ;
254
271
255
272
Ok ( ( ) )
256
273
}
257
274
258
275
fn to_child ( & mut self , name : & str ) -> StdResult < ( ) , NavigateError > {
259
276
let abs_path = self . abs_path . join ( name) ;
260
277
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) ?;
262
279
263
280
self . abs_path = abs_path;
264
281
self . rel_path = rel_path;
265
- self . module_path = module_path ;
282
+ self . resolved_path = resolved_path ;
266
283
267
284
Ok ( ( ) )
268
285
}
269
286
270
287
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 )
272
291
}
273
292
274
293
fn cache_key ( & self ) -> String {
275
- self . module_path . display ( ) . to_string ( )
294
+ self . resolved_path . as_deref ( ) . unwrap ( ) . display ( ) . to_string ( )
276
295
}
277
296
278
297
fn has_config ( & self ) -> bool {
@@ -285,7 +304,9 @@ impl Require for TextRequirer {
285
304
286
305
fn loader ( & self , lua : & Lua ) -> Result < Function > {
287
306
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 ( )
289
310
}
290
311
}
291
312
@@ -496,7 +517,7 @@ unsafe fn write_to_buffer(
496
517
}
497
518
498
519
#[ 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 > {
500
521
unsafe extern "C-unwind" fn find_current_file ( state : * mut ffi:: lua_State ) -> c_int {
501
522
let mut ar: ffi:: lua_Debug = mem:: zeroed ( ) ;
502
523
for level in 2 .. {
0 commit comments