diff --git a/llrt/src/main.rs b/llrt/src/main.rs index d4c1fea446..4c8c953522 100644 --- a/llrt/src/main.rs +++ b/llrt/src/main.rs @@ -123,7 +123,7 @@ async fn start_cli(vm: &Vm) { }, "-e" | "--eval" => { if let Some(source) = args.get(i + 1) { - vm.run(source.as_bytes(), false, true).await; + vm.run(source.as_bytes(), false, false).await; } return; }, @@ -192,7 +192,12 @@ async fn start_cli(vm: &Vm) { } } } else { - vm.run_file("index", true, false).await; + let index = if let Ok(dir) = std::env::current_dir() { + [dir.to_string_lossy().as_ref(), "/index"].concat() + } else { + "index".into() + }; + vm.run_file(index, true, false).await; } } diff --git a/llrt_core/src/module_loader/loader.rs b/llrt_core/src/module_loader/loader.rs index 8497cf75cf..240ea11467 100644 --- a/llrt_core/src/module_loader/loader.rs +++ b/llrt_core/src/module_loader/loader.rs @@ -15,6 +15,7 @@ use crate::{ BYTECODE_COMPRESSED, BYTECODE_FILE_EXT, BYTECODE_UNCOMPRESSED, BYTECODE_VERSION, SIGNATURE_LENGTH, }, + module_loader::CJS_LOADER_PREFIX, vm::COMPRESSION_DICT, }; @@ -95,49 +96,72 @@ impl CustomLoader { let cjs_specifier = [CJS_IMPORT_PREFIX, name].concat(); let require: Function = ctx.globals().get("require")?; let export_object: Value = require.call((&cjs_specifier,))?; - let mut module = String::from("const value = require(\""); + let mut module = String::with_capacity(name.len() + 512); + module.push_str("const value = require(\""); module.push_str(name); - module.push_str("\");export default value;"); + module.push_str("\");export default value.default||value;"); if let Some(obj) = export_object.as_object() { - module.push_str("const{"); let keys: Result> = obj.keys().collect(); let keys = keys?; - for (i, p) in keys.iter().enumerate() { - if i > 0 { + + if !keys.is_empty() { + module.push_str("const{"); + + for p in keys.iter() { + if p == "default" { + continue; + } + module.push_str(p); module.push(','); } - module.push_str(p); - } - module.push_str("}=value;"); - module.push_str("export{"); - for (i, p) in keys.iter().enumerate() { - if i > 0 { + module.truncate(module.len() - 1); + module.push_str("}=value;"); + module.push_str("export{"); + for p in keys.iter() { + if p == "default" { + continue; + } + module.push_str(p); module.push(','); } - module.push_str(p); + module.truncate(module.len() - 1); + module.push_str("};"); } - module.push_str("};"); } Module::declare(ctx, name, module) } - fn load_module<'js>(name: &str, ctx: &Ctx<'js>) -> Result<(Module<'js>, Option)> { - let mut from_cjs_import = false; - let path = if let Some(cjs_path) = name.strip_prefix(CJS_IMPORT_PREFIX) { - from_cjs_import = true; - cjs_path - } else { - name - }; + fn normalize_name(name: &str) -> (bool, bool, &str, &str) { + if !name.starts_with("__") { + // If name doesn’t start with "__", return defaults + return (false, false, name, name); + } + if let Some(cjs_path) = name.strip_prefix(CJS_IMPORT_PREFIX) { + // If it starts with CJS_IMPORT_PREFIX, mark as from_cjs_import + return (true, false, name, cjs_path); + } + + if let Some(cjs_path) = name.strip_prefix(CJS_LOADER_PREFIX) { + // If it starts with CJS_LOADER_PREFIX, mark as is_cjs + return (false, true, cjs_path, cjs_path); + } + + // Default return if no prefixes match + (false, false, name, name) + } + + fn load_module<'js>(name: &str, ctx: &Ctx<'js>) -> Result<(Module<'js>, Option)> { let ctx = ctx.clone(); - trace!("Loading module: {}", name); + let (from_cjs_import, is_cjs, normalized_name, path) = Self::normalize_name(name); + + trace!("Loading module: {}", normalized_name); //json files can never be from CJS imports as they are handled by require if !from_cjs_import { - if name.ends_with(".json") { + if normalized_name.ends_with(".json") { let mut file = File::open(path)?; let prefix = "export default JSON.parse(`"; let suffix = "`);"; @@ -148,9 +172,9 @@ impl CustomLoader { return Ok((Module::declare(ctx, path, json)?, None)); } - if name.ends_with(".cjs") { + if is_cjs || normalized_name.ends_with(".cjs") { let url = ["file://", path].concat(); - return Ok((Self::load_cjs_module(name, ctx)?, Some(url))); + return Ok((Self::load_cjs_module(normalized_name, ctx)?, Some(url))); } } @@ -166,7 +190,7 @@ impl CustomLoader { let bytes = std::fs::read(path)?; let mut bytes: &[u8] = &bytes; - if name.ends_with(BYTECODE_FILE_EXT) { + if normalized_name.ends_with(BYTECODE_FILE_EXT) { trace!("Loading binary module: {}", path); return Ok((Self::load_bytecode_module(ctx, bytes)?, Some(path.into()))); } @@ -175,7 +199,7 @@ impl CustomLoader { } let url = ["file://", path].concat(); - Ok((Module::declare(ctx, name, bytes)?, Some(url))) + Ok((Module::declare(ctx, normalized_name, bytes)?, Some(url))) } } diff --git a/llrt_core/src/module_loader/mod.rs b/llrt_core/src/module_loader/mod.rs index 95b537aa0e..26f3cc2b00 100644 --- a/llrt_core/src/module_loader/mod.rs +++ b/llrt_core/src/module_loader/mod.rs @@ -1,4 +1,7 @@ pub mod loader; pub mod resolver; +// added when .cjs files are imported pub const CJS_IMPORT_PREFIX: &str = "__cjs:"; +// added to force CJS imports in loader +pub const CJS_LOADER_PREFIX: &str = "__cjsm:"; diff --git a/llrt_core/src/module_loader/resolver.rs b/llrt_core/src/module_loader/resolver.rs index d9818e6d7b..7e0fdd42be 100644 --- a/llrt_core/src/module_loader/resolver.rs +++ b/llrt_core/src/module_loader/resolver.rs @@ -15,11 +15,11 @@ use llrt_modules::path::{ use llrt_utils::result::ResultExt; use once_cell::sync::Lazy; use rquickjs::{loader::Resolver, Ctx, Error, Result}; -use simd_json::BorrowedValue; +use simd_json::{derived::ValueObjectAccessAsScalar, BorrowedValue}; use tracing::trace; use crate::{ - bytecode::BYTECODE_FILE_EXT, + module_loader::CJS_LOADER_PREFIX, utils::io::{is_supported_ext, JS_EXTENSIONS, SUPPORTED_EXTENSIONS}, }; @@ -108,6 +108,9 @@ pub fn require_resolve<'a>( return resolved_by_file_exists(x_normalized.into()); } + let x_is_absolute = path::is_absolute(x); + let x_starts_with_current_dir = x.starts_with("./"); + // 2. If X begins with '/' let y = if path::is_absolute(x) { // a. set Y to be the file system root @@ -118,16 +121,12 @@ pub fn require_resolve<'a>( // Normalize path Y to generate dirname(Y) let dirname_y = if Path::new(y).is_dir() { - path::resolve_path([y].iter()) + path::resolve_path([y].iter())? } else { let dirname_y = path::dirname(y); - path::resolve_path([&dirname_y].iter()) + path::resolve_path([&dirname_y].iter())? }; - let x_is_absolute = path::is_absolute(x); - - let x_starts_with_current_dir = x.starts_with("./"); - // 3. If X begins with './' or '/' or '../' if x_starts_with_current_dir || x_is_absolute || x.starts_with("../") { let y_plus_x = if x_is_absolute { @@ -143,12 +142,12 @@ pub fn require_resolve<'a>( // a. LOAD_AS_FILE(Y + X) if let Ok(Some(path)) = load_as_file(ctx, y_plus_x.clone()) { trace!("+- Resolved by `LOAD_AS_FILE`: {}\n", path); - return Ok(to_abs_path(path)); + return to_abs_path(path); } else { // b. LOAD_AS_DIRECTORY(Y + X) if let Ok(Some(path)) = load_as_directory(ctx, y_plus_x) { trace!("+- Resolved by `LOAD_AS_DIRECTORY`: {}\n", path); - return Ok(to_abs_path(path)); + return to_abs_path(path); } } @@ -168,7 +167,7 @@ pub fn require_resolve<'a>( // 5. LOAD_PACKAGE_SELF(X, dirname(Y)) if let Ok(Some(path)) = load_package_self(ctx, x, &dirname_y, is_esm) { trace!("+- Resolved by `LOAD_PACKAGE_SELF`: {}\n", path); - return Ok(to_abs_path(path.into())); + return to_abs_path(path.into()); } // 6. LOAD_NODE_MODULES(X, dirname(Y)) @@ -180,7 +179,7 @@ pub fn require_resolve<'a>( // 6.5. LOAD_AS_FILE(X) if let Ok(Some(path)) = load_as_file(ctx, Rc::new(x.to_owned())) { trace!("+- Resolved by `LOAD_AS_FILE`: {}\n", path); - return Ok(to_abs_path(path)); + return to_abs_path(path); } // 7. THROW "not found" @@ -194,17 +193,17 @@ fn resolved_by_bytecode_cache(x: Cow<'_, str>) -> Result> { fn resolved_by_file_exists(path: Cow<'_, str>) -> Result> { trace!("+- Resolved by `FILE`: {}\n", path); - Ok(to_abs_path(path)) + to_abs_path(path) } -fn to_abs_path(path: Cow<'_, str>) -> Cow<'_, str> { - if !is_absolute(&path) { - resolve_path_with_separator([path], true).into() +fn to_abs_path(path: Cow<'_, str>) -> Result> { + Ok(if !is_absolute(&path) { + resolve_path_with_separator([path], true)?.into() } else if cfg!(windows) { replace_backslash(path).into() } else { path - } + }) } // LOAD_AS_FILE(X) @@ -267,7 +266,7 @@ fn load_index<'a>(ctx: &Ctx<'_>, x: Rc) -> Result>> trace!("| load_index(x): {}", x); // 1. If X/index.js is a file - for extension in [".js", ".mjs", ".cjs", BYTECODE_FILE_EXT].iter() { + for extension in SUPPORTED_EXTENSIONS.iter() { let file = [x.as_str(), "/index", extension].concat(); if Path::new(&file).is_file() { // a. Find the closest package scope SCOPE to X. @@ -428,17 +427,37 @@ fn load_package_exports<'a>( //2. If X does not match this pattern or DIR/NAME/package.json is not a file, // return. - let mut package_json_path = [dir, "/"].concat(); + let mut package_json_path = String::with_capacity(dir.len() + 64); + package_json_path.push_str(dir); + package_json_path.push('/'); let base_path_length = package_json_path.len(); package_json_path.push_str(scope); package_json_path.push_str("/package.json"); + let mut sub_module = None; + let (scope, name) = if name != "." && !Path::new(&package_json_path).exists() { package_json_path.truncate(base_path_length); package_json_path.push_str(x); package_json_path.push_str("/package.json"); (x, ".") } else { + for ext in JS_EXTENSIONS { + let path = [ + &package_json_path[0..base_path_length], + scope, + name.as_ref().trim_start_matches("."), + *ext, + ] + .concat(); + if Path::new(&path).exists() { + if *ext == ".mjs" { + //we know its an ESM module + return Ok(path.into()); + } + sub_module = Some(path); + } + } (scope, name.as_ref()) }; @@ -454,6 +473,13 @@ fn load_package_exports<'a>( let mut package_json = fs::read(&package_json_path).or_throw(ctx)?; let package_json = simd_json::to_borrowed_value(&mut package_json).or_throw(ctx)?; + if let Some(sub_module) = sub_module { + if package_json.get_str("type") != Some("module") { + return Ok([CJS_LOADER_PREFIX, &sub_module].concat().into()); + } + return Ok(sub_module.into()); + } + let module_path = package_exports_resolve(&package_json, name, is_esm)?; Ok(correct_extensions( diff --git a/llrt_core/src/modules/js/@llrt/test/SocketClient.ts b/llrt_core/src/modules/js/@llrt/test/SocketClient.ts index b2de237769..ad6742b542 100644 --- a/llrt_core/src/modules/js/@llrt/test/SocketClient.ts +++ b/llrt_core/src/modules/js/@llrt/test/SocketClient.ts @@ -33,8 +33,6 @@ class SocketClient extends EventEmitter { const errorListener = (err: Error) => reject(err); this.socket.on("error", errorListener); this.socket.connect(this.port, this.host, () => { - console.log(`Connected to ${this.host}:${this.port}`); - this.socket.off("error", errorListener); this.socket.on("error", (err) => this.emit("error", err)); resolve(); diff --git a/llrt_core/src/vm.rs b/llrt_core/src/vm.rs index b2679c52b8..ae2b8af0f2 100644 --- a/llrt_core/src/vm.rs +++ b/llrt_core/src/vm.rs @@ -366,7 +366,7 @@ fn init(ctx: &Ctx<'_>, module_names: HashSet<&'static str>) -> Result<()> { } else { let module_name = get_script_or_module_name(&ctx); let module_name = module_name.trim_start_matches(CJS_IMPORT_PREFIX); - let abs_path = resolve_path([module_name].iter()); + let abs_path = resolve_path([module_name].iter())?; let resolved_path = require_resolve(&ctx, &specifier, &abs_path, false)?.into_owned(); diff --git a/modules/llrt_path/src/lib.rs b/modules/llrt_path/src/lib.rs index 2a827457a0..d53bdf94bb 100644 --- a/modules/llrt_path/src/lib.rs +++ b/modules/llrt_path/src/lib.rs @@ -257,7 +257,7 @@ where join_resolve_path(parts_vec, false, result, PathBuf::new(), force_posix_sep) } -pub fn resolve_path(parts: I) -> String +pub fn resolve_path(parts: I) -> Result where S: AsRef, I: IntoIterator, @@ -265,12 +265,12 @@ where resolve_path_with_separator(parts, false) } -pub fn resolve_path_with_separator(parts: I, force_posix_sep: bool) -> String +pub fn resolve_path_with_separator(parts: I, force_posix_sep: bool) -> Result where S: AsRef, I: IntoIterator, { - let cwd = std::env::current_dir().expect("Unable to access working directory"); + let cwd = std::env::current_dir()?; let mut result = cwd.clone().into_os_string().into_string().unwrap(); //add MAIN_SEPARATOR if we're not on already MAIN_SEPARATOR @@ -283,10 +283,10 @@ where result = result.replace(MAIN_SEPARATOR, FORWARD_SLASH_STR); } } - join_resolve_path(parts, true, result, cwd, force_posix_sep) + Ok(join_resolve_path(parts, true, result, cwd, force_posix_sep)) } -pub fn relative(from: F, to: T) -> String +pub fn relative(from: F, to: T) -> Result where F: AsRef, T: AsRef, @@ -294,19 +294,14 @@ where let from_ref = from.as_ref(); let to_ref = to.as_ref(); if from_ref == to_ref { - return "".to_string(); + return Ok("".into()); } let mut abs_from = None; if !is_absolute(from_ref) { abs_from = Some( - std::env::current_dir() - .expect("Unable to access working directory") - .to_string_lossy() - .to_string() - + MAIN_SEPARATOR_STR - + from_ref, + std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + from_ref, ); } @@ -314,12 +309,7 @@ where if !is_absolute(to_ref) { abs_to = Some( - std::env::current_dir() - .expect("Unable to access working directory") - .to_string_lossy() - .to_string() - + MAIN_SEPARATOR_STR - + to_ref, + std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + to_ref, ); } @@ -366,11 +356,11 @@ where } to_index = to_next + 1; // Move past the separator } - if relative.is_empty() { - ".".to_string() + Ok(if relative.is_empty() { + ".".into() } else { relative - } + }) } fn join_resolve_path( @@ -471,7 +461,7 @@ where result } -pub fn resolve(path: Rest) -> String { +pub fn resolve(path: Rest) -> Result { resolve_path(path.iter()) } @@ -565,10 +555,7 @@ impl From for ModuleInfo { #[cfg(test)] mod tests { - use std::{ - env::{current_dir, set_current_dir}, - sync::Mutex, - }; + use std::{env::set_current_dir, sync::Mutex}; static THREAD_LOCK: Lazy> = Lazy::new(Mutex::default); @@ -579,30 +566,30 @@ mod tests { #[test] fn test_relative() { let _shared = THREAD_LOCK.lock().unwrap(); - let cwd = current_dir().expect("Unable to access working directory"); + let cwd = std::env::current_dir().expect("unable to get current working directory"); set_current_dir("/").expect("unable to set working directory to /"); assert_eq!( - relative("a/b/c", "b/c"), + relative("a/b/c", "b/c").unwrap(), "../../../b/c".replace('/', MAIN_SEPARATOR_STR) ); assert_eq!( - relative("/data/orandea/test/aaa", "/data/orandea/impl/bbb"), + relative("/data/orandea/test/aaa", "/data/orandea/impl/bbb").unwrap(), "../../impl/bbb".replace('/', MAIN_SEPARATOR_STR) ); assert_eq!( - relative("/a/b/c", "/a/d"), + relative("/a/b/c", "/a/d").unwrap(), "../../d".replace('/', MAIN_SEPARATOR_STR) ); - assert_eq!(relative("/a/b/c", "/a/b/c/d"), "d"); - assert_eq!(relative("/a/b/c", "/a/b/c"), ""); + assert_eq!(relative("/a/b/c", "/a/b/c/d").unwrap(), "d"); + assert_eq!(relative("/a/b/c", "/a/b/c").unwrap(), ""); assert_eq!( - relative("a/b", "a/b/c/d"), + relative("a/b", "a/b/c/d").unwrap(), "c/d".replace('/', MAIN_SEPARATOR_STR) ); assert_eq!( - relative("a/b/c", "b/c"), + relative("a/b/c", "b/c").unwrap(), "../../../b/c".replace('/', MAIN_SEPARATOR_STR) ); @@ -705,7 +692,7 @@ mod tests { }; assert_eq!( - resolve_path(["", "foo/bar"].iter()), + resolve_path(["", "foo/bar"].iter()).unwrap(), std::env::current_dir() .unwrap() .join("foo/bar".replace('/', MAIN_SEPARATOR_STR)) @@ -715,51 +702,51 @@ mod tests { // Standard cases assert_eq!( - resolve_path(["/"].iter()), + resolve_path(["/"].iter()).unwrap(), prefix.clone() + MAIN_SEPARATOR_STR ); // Standard cases assert_eq!( - resolve_path(["/foo/bar", "../baz"].iter()), + resolve_path(["/foo/bar", "../baz"].iter()).unwrap(), prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR) ); assert_eq!( - resolve_path(["/foo/bar", "./baz"].iter()), + resolve_path(["/foo/bar", "./baz"].iter()).unwrap(), prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR) ); assert_eq!( - resolve_path(["foo/bar", "/baz"].iter()), + resolve_path(["foo/bar", "/baz"].iter()).unwrap(), prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR) ); // Complex cases assert_eq!( - resolve_path(["/foo", "bar", ".", "baz"].iter()), + resolve_path(["/foo", "bar", ".", "baz"].iter()).unwrap(), prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR) ); // Current dir in middle assert_eq!( - resolve_path(["/foo", "bar", "..", "baz"].iter()), + resolve_path(["/foo", "bar", "..", "baz"].iter()).unwrap(), prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR) ); // Parent dir in middle assert_eq!( - resolve_path(["/foo", "bar", "../..", "baz"].iter()), + resolve_path(["/foo", "bar", "../..", "baz"].iter()).unwrap(), prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR) ); // Double parent dir assert_eq!( - resolve_path(["/foo", "bar", ".hidden"].iter()), + resolve_path(["/foo", "bar", ".hidden"].iter()).unwrap(), prefix.clone() + &"/foo/bar/.hidden".replace('/', MAIN_SEPARATOR_STR) ); // Hidden file assert_eq!( - resolve_path(["/foo", ".", "bar", "."].iter()), + resolve_path(["/foo", ".", "bar", "."].iter()).unwrap(), prefix.clone() + &"/foo/bar".replace('/', MAIN_SEPARATOR_STR) ); // Multiple current dirs assert_eq!( - resolve_path(["/foo", "..", "..", "bar"].iter()), + resolve_path(["/foo", "..", "..", "bar"].iter()).unwrap(), prefix.clone() + &"/bar".replace('/', MAIN_SEPARATOR_STR) ); // Multiple parent dirs assert_eq!( - resolve_path(["/foo/bar", "/..", "baz"].iter()), + resolve_path(["/foo/bar", "/..", "baz"].iter()).unwrap(), prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR) ); // Parent dir with absolute path } diff --git a/tests/unit/require.test.ts b/tests/unit/require.test.ts index 24b20e8890..f16669c0ed 100644 --- a/tests/unit/require.test.ts +++ b/tests/unit/require.test.ts @@ -1,6 +1,9 @@ globalThis._require = require; //used to preserve require during bundling/minification - const CWD = process.cwd(); +import { spawn } from "child_process"; + +import { platform } from "os"; +const IS_WIN = platform() === "win32"; it("should require a file (absolute path)", () => { const { hello } = _require(`${CWD}/fixtures/hello.js`); @@ -111,3 +114,15 @@ it("should handle inner referenced exports", () => { expect(a.cat()).toBe("str"); expect(a.length()).toBe(1); }); + +if (!IS_WIN) { + it("should handle named exports from CJS imports", (cb) => { + spawn(process.argv0, [ + "-e", + `import {cat} from "${CWD}/fixtures/referenced-exports.cjs"`, + ]).on("close", (code) => { + expect(code).toBe(0); + cb(); + }); + }); +}