diff --git a/Cargo.lock b/Cargo.lock index 9d07af88ce..0b841653a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2632,6 +2632,7 @@ dependencies = [ "criterion", "flate2", "fxhash", + "home", "http-body-util", "hyper", "hyper-rustls", diff --git a/Makefile b/Makefile index c0f4b6419f..5454e554a0 100644 --- a/Makefile +++ b/Makefile @@ -184,8 +184,10 @@ test-e2e: js cargo run -- test -d bundle/js/__tests__/e2e test-ci: export JS_MINIFY = 0 +test-ci: export RUST_BACKTRACE = 1 +test-ci: export RUST_LOG = trace test-ci: clean-js | toolchain js - cargo $(TOOLCHAIN) -Z panic-abort-tests test --target $(CURRENT_TARGET) + cargo $(TOOLCHAIN) -Z panic-abort-tests test --target $(CURRENT_TARGET) -- --nocapture --show-output cargo $(TOOLCHAIN) run -r --target $(CURRENT_TARGET) -- test -d bundle/js/__tests__/unit libs-arm64: lib/arm64/libzstd.a lib/zstd.h diff --git a/llrt_core/Cargo.toml b/llrt_core/Cargo.toml index ea8d7b92c7..f45275632c 100644 --- a/llrt_core/Cargo.toml +++ b/llrt_core/Cargo.toml @@ -79,6 +79,7 @@ flate2 = { version = "1.0.30", features = [ ], default-features = false } brotlic = "0.8.2" rustls-pemfile = "2.1.2" +home = "0.5.9" [build-dependencies] rquickjs = { version = "0.6.2", features = [ diff --git a/llrt_core/src/custom_resolver.rs b/llrt_core/src/custom_resolver.rs new file mode 100644 index 0000000000..551da2fa24 --- /dev/null +++ b/llrt_core/src/custom_resolver.rs @@ -0,0 +1,533 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{ + collections::HashMap, + env, fs, + path::{Path, PathBuf}, + sync::Mutex, +}; + +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 tracing::trace; + +include!(concat!(env!("OUT_DIR"), "/bytecode_cache.rs")); + +static NODE_MODULES_PATHS_CACHE: Lazy>>>> = + Lazy::new(|| Mutex::new(HashMap::new())); + +static FILESYSTEM_ROOT: Lazy> = Lazy::new(|| { + #[cfg(unix)] + { + "/".into() + } + #[cfg(windows)] + { + if let Some(path) = home::home_dir() { + let components: Vec<_> = path.components().collect(); + if let Some(component) = components.get(0) { + if let std::path::Component::Prefix(prefix) = component { + return prefix + .as_os_str() + .to_string_lossy() + .into_owned() + .into_boxed_str(); + } + } + } + "C:".to_string().into_boxed_str() + } +}); + +#[derive(Debug, Default)] +pub struct CustomResolver; + +#[allow(clippy::manual_strip)] +impl Resolver for CustomResolver { + fn resolve(&mut self, ctx: &Ctx, base: &str, name: &str) -> Result { + trace!("Try resolve '{}' from '{}'", name, base); + require_resolve(ctx, name, base, true) + } +} + +// [CJS Reference Implementation](https://nodejs.org/api/modules.html#all-together) +// require(X) from module at path Y +pub fn require_resolve(ctx: &Ctx<'_>, x: &str, y: &str, is_esm: bool) -> Result { + trace!("require_resolve(x, y):({}, {})", x, y); + + // 1. If X is a core module, + // a. return the core module + // b. STOP + + // 1'. If X is a bytecode cache, + for check_x in [x, &path::normalize(x.to_string())].iter() { + if BYTECODE_CACHE.contains_key(check_x) { + // a. return the bytecode cache + // b. STOP + trace!("+- Resolved by `BYTECODE_CACHE`: {}\n", check_x); + return Ok(check_x.to_string()); + } + } + + // 2. If X begins with '/' + let y = if path::is_absolute(x) { + // a. set Y to be the file system root + &*FILESYSTEM_ROOT + } else { + y + }; + + // Normalize path Y to generate dirname(Y) + let dirname_y = if Path::new(y).is_dir() { + path::resolve_path([y].iter()) + } else { + let dirname_y = path::dirname(y.to_string()); + path::resolve_path([&dirname_y].iter()) + }; + + // 3. If X begins with './' or '/' or '../' + if x.starts_with("./") || path::is_absolute(x) || x.starts_with("../") { + let y_plus_x = if cfg!(windows) && path::is_absolute(x) { + x.to_string() + } else { + [&dirname_y, "/", x].concat() + }; + let y_plus_x = y_plus_x.as_str(); + // a. LOAD_AS_FILE(Y + X) + if let Ok(Some(path)) = load_as_file(ctx, y_plus_x) { + trace!("+- Resolved by `LOAD_AS_FILE`: {}\n", path); + return Ok(path.to_string()); + } + // 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(path.to_string()); + } + // c. THROW "not found" + return Err(Error::new_resolving(y.to_string(), x.to_string())); + } + + // 4. If X begins with '#' + if x.starts_with('#') { + // a. LOAD_PACKAGE_IMPORTS(X, dirname(Y)) + if let Ok(Some(path)) = load_package_imports(ctx, x, &dirname_y, is_esm) { + trace!("+- Resolved by `LOAD_PACKAGE_IMPORTS`: {}\n", path); + return Ok(path.to_string()); + } + } + + // 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(path.to_string()); + } + + // 6. LOAD_NODE_MODULES(X, dirname(Y)) + if let Some(path) = load_node_modules(ctx, x, &dirname_y, is_esm) { + trace!("+- Resolved by `LOAD_NODE_MODULES`: {}\n", path); + return Ok(path.to_string()); + } + + // 7. THROW "not found" + Err(Error::new_resolving(y.to_string(), x.to_string())) +} + +// LOAD_AS_FILE(X) +fn load_as_file(ctx: &Ctx<'_>, x: &str) -> Result>> { + trace!("| load_as_file(x): {}", x); + + // 1. If X is a file, load X as its file extension format. STOP + if Path::new(&x).is_file() { + trace!("| load_as_file(1): {}", x); + return Ok(Some(Box::from(x))); + } + + // 2. If X.js is a file, + for extension in [".js", ".mjs", ".cjs", ".lrt"].iter() { + let file = [x, extension].concat(); + if Path::new(&file).is_file() { + // a. Find the closest package scope SCOPE to X. + match find_the_closest_package_scope(x) { + // b. If no scope was found + None => { + // 1. MAYBE_DETECT_AND_LOAD(X.js) + trace!("| load_as_file(2.b.1): {}", file); + return Ok(Some(Box::from(file))); + }, + Some(path) => { + let mut package_json = fs::read(path.as_ref()).or_throw(ctx)?; + let package_json = + simd_json::to_borrowed_value(&mut package_json).or_throw(ctx)?; + // c. If the SCOPE/package.json contains "type" field, + if let Some(_type) = get_string_field(&package_json, "type") { + // 1. If the "type" field is "module", load X.js as an ECMAScript module. STOP. + // 2. If the "type" field is "commonjs", load X.js as an CommonJS module. STOP. + if _type == "module" || _type == "commonjs" { + trace!("| load_as_file(2.c.[1|2]): {}", file); + return Ok(Some(Box::from(file))); + } + } + }, + } + // d. MAYBE_DETECT_AND_LOAD(X.js) + trace!("| load_as_file(2.d): {}", file); + return Ok(Some(Box::from(file))); + } + } + + // 3. If X.json is a file, load X.json to a JavaScript Object. STOP + let file = [x, ".json"].concat(); + if Path::new(&file).is_file() { + trace!("| load_as_file(3): {}", file); + return Ok(Some(Box::from(file))); + } + + // 4. If X.node is a file, load X.node as binary addon. STOP + + Ok(None) +} + +// LOAD_INDEX(X) +fn load_index(ctx: &Ctx<'_>, x: &str) -> Result>> { + trace!("| load_index(x): {}", x); + + // 1. If X/index.js is a file + for extension in [".js", ".mjs", ".cjs", ".lrt"].iter() { + let file = [x, "/index", extension].concat(); + if Path::new(&file).is_file() { + // a. Find the closest package scope SCOPE to X. + match find_the_closest_package_scope(x) { + // b. If no scope was found, load X/index.js as a CommonJS module. STOP. + None => { + trace!("| load_index(1.b): {}", file); + return Ok(Some(Box::from(file))); + }, + // c. If the SCOPE/package.json contains "type" field, + Some(path) => { + let mut package_json = fs::read(path.as_ref()).or_throw(ctx)?; + let package_json = + simd_json::to_borrowed_value(&mut package_json).or_throw(ctx)?; + if let Some(_type) = get_string_field(&package_json, "type") { + // 1. If the "type" field is "module", load X/index.js as an ECMAScript module. STOP. + if _type == "module" { + trace!("| load_index(1.c.1): {}", file); + return Ok(Some(Box::from(file))); + } + } + // 2. Else, load X/index.js as an CommonJS module. STOP. + trace!("| load_index(1.c.2): {}", file); + return Ok(Some(Box::from(file))); + }, + } + } + } + + // 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP + let file = [x, "/index.json"].concat(); + if Path::new(&file).is_file() { + trace!("| load_index(2): {}", file); + return Ok(Some(Box::from(file))); + } + + // 3. If X/index.node is a file, load X/index.node as binary addon. STOP + + Ok(None) +} + +// LOAD_AS_DIRECTORY(X) +fn load_as_directory(ctx: &Ctx<'_>, x: &str) -> Result>> { + trace!("| load_as_directory(x): {}", x); + + // 1. If X/package.json is a file, + let file = [x, "/package.json"].concat(); + if Path::new(&file).is_file() { + // a. Parse X/package.json, and look for "main" field. + let mut package_json = fs::read(file).or_throw(ctx)?; + let package_json = simd_json::to_borrowed_value(&mut package_json).or_throw(ctx)?; + // b. If "main" is a falsy value, GOTO 2. + if let Some(main) = get_string_field(&package_json, "main") { + // c. let M = X + (json main field) + let m = [x, "/", main].concat(); + // d. LOAD_AS_FILE(M) + if let Ok(Some(path)) = load_as_file(ctx, &m) { + trace!("| load_as_directory(1.d): {}", path); + return Ok(Some(path)); + } + // e. LOAD_INDEX(M) + if let Ok(Some(path)) = load_index(ctx, &m) { + trace!("| load_as_directory(1.e): {}", path); + return Ok(Some(path)); + } + // f. LOAD_INDEX(X) DEPRECATED + + // g. THROW "not found" + return Err(Error::new_resolving("", x.to_string())); + } + } + + // 2. LOAD_INDEX(X) + if let Ok(Some(path)) = load_index(ctx, x) { + trace!("| load_as_directory(2): {}", path); + return Ok(Some(path)); + } + + Ok(None) +} + +// LOAD_NODE_MODULES(X, START) +fn load_node_modules(ctx: &Ctx<'_>, x: &str, start: &str, is_esm: bool) -> Option> { + trace!("| load_node_modules(x, start): ({}, {})", x, start); + + // 1. let DIRS = NODE_MODULES_PATHS(START) + let mut cache = NODE_MODULES_PATHS_CACHE.lock().unwrap(); + let dirs = cache + .entry(start.to_string()) + .or_insert_with(|| node_modules_paths(start)); + + // 2. for each DIR in DIRS: + for dir in dirs { + // a. LOAD_PACKAGE_EXPORTS(X, DIR) + if let Ok(path) = load_package_exports(ctx, x, dir, is_esm) { + trace!("| load_node_modules(2.a): {}", path); + return Some(path); + } + let dir_slash_x = [dir, "/", x].concat(); + // b. LOAD_AS_FILE(DIR/X) + if let Ok(Some(path)) = load_as_file(ctx, &dir_slash_x) { + trace!("| load_node_modules(2.b): {}", path); + return Some(path); + } + // c. LOAD_AS_DIRECTORY(DIR/X) + if let Ok(Some(path)) = load_as_directory(ctx, &dir_slash_x) { + trace!("| load_node_modules(2.c): {}", path); + return Some(path); + } + } + + None +} + +// NODE_MODULES_PATHS(START) +fn node_modules_paths(start: &str) -> Vec> { + let path = Path::new(start); + let mut dirs = Vec::new(); + let mut current = Some(path); + + // Iterate through parent directories + while let Some(dir) = current { + if dir.file_name().map_or(false, |name| name != "node_modules") { + let mut node_modules = dir.to_path_buf(); + node_modules.push("node_modules"); + dirs.push(Box::from(node_modules.to_string_lossy())); + } + current = dir.parent(); + } + + // Add global folders + if let Some(home) = home::home_dir() { + dirs.push(Box::from(home.join(".node_modules").to_string_lossy())); + dirs.push(Box::from(home.join(".node_libraries").to_string_lossy())); + } + + dirs +} + +// LOAD_PACKAGE_IMPORTS(X, DIR) +fn load_package_imports( + _ctx: &Ctx<'_>, + x: &str, + dir: &str, + _is_esm: bool, +) -> Result>> { + trace!("| load_package_imports(x, dir): ({}, {})", x, dir); + // 1. Find the closest package scope SCOPE to DIR. + // 2. If no scope was found, return. + // 3. If the SCOPE/package.json "imports" is null or undefined, return. + // 4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), + // ["node", "require"]) defined in the ESM resolver. + // 5. RESOLVE_ESM_MATCH(MATCH). + Ok(None) +} + +// LOAD_PACKAGE_EXPORTS(X, DIR) +fn load_package_exports(ctx: &Ctx<'_>, x: &str, dir: &str, is_esm: bool) -> Result> { + trace!("| load_package_exports(x, dir): ({}, {})", x, dir); + //1. Try to interpret X as a combination of NAME and SUBPATH where the name + // may have a @scope/ prefix and the subpath begins with a slash (`/`). + let (scope, name) = match x.split_once('/') { + Some((s, n)) => (s, ["./", n].concat()), + None => (x, ".".to_string()), + }; + + //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 base_path_length = package_json_path.len(); + package_json_path.push_str(scope); + package_json_path.push_str("/package.json"); + + 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 { + (scope, name.as_str()) + }; + + if !Path::new(&package_json_path).exists() { + return Err(Error::new_resolving(dir.to_string(), x.to_string())); + }; + + //3. Parse DIR/NAME/package.json, and look for "exports" field. + //4. If "exports" is null or undefined, return. + //5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, + // `package.json` "exports", ["node", "require"]) defined in the ESM resolver. + //6. RESOLVE_ESM_MATCH(MATCH) + 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)?; + + let module_path = package_exports_resolve(&package_json, name, is_esm)?; + + Ok(Box::from([dir, "/", scope, "/", module_path].concat())) +} + +// LOAD_PACKAGE_SELF(X, DIR) +fn load_package_self(ctx: &Ctx<'_>, x: &str, dir: &str, is_esm: bool) -> Result>> { + trace!("| load_package_self(x, dir): ({}, {})", x, dir); + + let (scope, name) = match x.split_once('/') { + Some((s, n)) => (s, ["./", n].concat()), + None => (x, ".".to_string()), + }; + let name = name.as_str(); + + // 1. Find the closest package scope SCOPE to DIR. + let mut package_json_file: Vec; + let package_json: BorrowedValue; + match find_the_closest_package_scope(dir) { + // 2. If no scope was found, return. + None => { + return Ok(None); + }, + Some(path) => { + package_json_file = fs::read(path.as_ref()).or_throw(ctx)?; + package_json = simd_json::to_borrowed_value(&mut package_json_file).or_throw(ctx)?; + // 3. If the SCOPE/package.json "exports" is null or undefined, return. + if !is_exports_field_exists(&package_json) { + return Ok(None); + } + // 4. If the SCOPE/package.json "name" is not the first segment of X, return. + if let Some(name) = get_string_field(&package_json, "name") { + if name != scope { + return Ok(None); + } + } + }, + }; + // 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE), + // "." + X.slice("name".length), `package.json` "exports", ["node", "require"]) + // defined in the ESM resolver. + // 6. RESOLVE_ESM_MATCH(MATCH) + if let Ok(path) = package_exports_resolve(&package_json, name, is_esm) { + trace!("| load_package_self(2.c): {}", path); + return Ok(Some(Box::from(path))); + } + + Ok(None) +} + +// Implementation equivalent to PACKAGE_EXPORTS_RESOLVE including RESOLVE_ESM_MATCH +fn package_exports_resolve<'a>( + package_json: &'a BorrowedValue<'a>, + modules_name: &str, + is_esm: bool, +) -> Result<&'a str> { + let ident = if is_esm { "import" } else { "require" }; + + if let BorrowedValue::Object(map) = package_json { + if let Some(BorrowedValue::Object(exports)) = map.get("exports") { + if let Some(BorrowedValue::Object(name)) = exports.get(modules_name) { + // Check for exports -> name -> [import | require] -> default + if let Some(BorrowedValue::Object(ident)) = name.get(ident) { + if let Some(BorrowedValue::String(default)) = ident.get("default") { + return Ok(default.as_ref()); + } + } + // Check for exports -> name -> [import | require] + if let Some(BorrowedValue::String(ident)) = name.get(ident) { + return Ok(ident.as_ref()); + } + // [CJS only] Check for exports -> name -> default + if !is_esm { + if let Some(BorrowedValue::String(default)) = name.get("default") { + return Ok(default.as_ref()); + } + } + } + // Check for exports -> [import | require] -> default + if let Some(BorrowedValue::Object(ident)) = exports.get(ident) { + if let Some(BorrowedValue::String(default)) = ident.get("default") { + return Ok(default.as_ref()); + } + } + // Check for exports -> [import | require] + if let Some(BorrowedValue::String(ident)) = exports.get(ident) { + return Ok(ident.as_ref()); + } + // [CJS only] Check for exports -> default + if !is_esm { + if let Some(BorrowedValue::String(default)) = exports.get("default") { + return Ok(default.as_ref()); + } + } + } + // [ESM only] Check for module field + if is_esm { + if let Some(BorrowedValue::String(module)) = map.get("module") { + return Ok(module.as_ref()); + } + } + // Check for main field + // Workaround for modules that have only “main” defined and whose entrypoint is not “index.js” + if let Some(BorrowedValue::String(main)) = map.get("main") { + return Ok(main.as_ref()); + } + } + Ok("./index.js") +} + +fn find_the_closest_package_scope(start: &str) -> Option> { + let mut current_dir = PathBuf::from(start); + loop { + let package_json_path = current_dir.join("package.json"); + if package_json_path.exists() { + return package_json_path.to_str().map(Box::from); + } + if !current_dir.pop() { + break; + } + } + None +} + +fn get_string_field<'a>(package_json: &'a BorrowedValue<'a>, str: &str) -> Option<&'a str> { + if let BorrowedValue::Object(map) = package_json { + if let Some(BorrowedValue::String(val)) = map.get(str) { + return Some(val.as_ref()); + } + } + None +} + +fn is_exports_field_exists<'a>(package_json: &'a BorrowedValue<'a>) -> bool { + if let BorrowedValue::Object(map) = package_json { + if let Some(BorrowedValue::Object(_)) = map.get("exports") { + return true; + } + } + false +} diff --git a/llrt_core/src/json_loader.rs b/llrt_core/src/json_loader.rs deleted file mode 100644 index bdf2dd0196..0000000000 --- a/llrt_core/src/json_loader.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rquickjs::{loader::Loader, Ctx, Error, Module, Result}; - -#[derive(Debug)] -pub struct JSONLoader; - -impl Loader for JSONLoader { - fn load<'js>(&mut self, ctx: &Ctx<'js>, path: &str) -> Result> { - if !path.ends_with(".json") { - return Err(Error::new_loading(path)); - } - let source = std::fs::read_to_string(path)?; - Module::declare(ctx.clone(), path, ["export default ", &source].concat()) - } -} diff --git a/llrt_core/src/lib.rs b/llrt_core/src/lib.rs index c8435f4945..ee9573d48b 100644 --- a/llrt_core/src/lib.rs +++ b/llrt_core/src/lib.rs @@ -13,7 +13,7 @@ mod compiler_common; pub mod environment; pub mod json; // mod minimal_tracer; -pub mod json_loader; +mod custom_resolver; mod module_builder; pub mod modules; pub mod number; diff --git a/llrt_core/src/vm.rs b/llrt_core/src/vm.rs index 94466a41fd..a5a94896f7 100644 --- a/llrt_core/src/vm.rs +++ b/llrt_core/src/vm.rs @@ -6,7 +6,7 @@ use std::{ env, ffi::CStr, fmt::Write, - fs, io, + io, path::{Path, PathBuf}, process::exit, rc::Rc, @@ -15,42 +15,36 @@ use std::{ }; use llrt_modules::{ - path::{is_absolute, join_path_with_separator, resolve_path, resolve_path_with_separator}, + path::{resolve_path, resolve_path_with_separator}, timers::{self, poll_timers}, }; -use llrt_utils::{ - bytes::ObjectBytes, error::ErrorExtensions, object::ObjectExt, result::ResultExt, -}; +use llrt_utils::{bytes::ObjectBytes, error::ErrorExtensions, object::ObjectExt}; use once_cell::sync::Lazy; use ring::rand::SecureRandom; use rquickjs::{ atom::PredefinedAtom, context::EvalOptions, function::Opt, - loader::{BuiltinLoader, FileResolver, Loader, Resolver, ScriptLoader}, + loader::{BuiltinLoader, FileResolver, Loader, ScriptLoader}, module::Declared, object::Accessor, prelude::{Func, Rest}, - qjs, AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Ctx, Error, Exception, Function, - IntoJs, Module, Object, Result, Value, + qjs, AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Ctx, Error, Function, IntoJs, + Module, Object, Result, Value, }; -use simd_json::BorrowedValue; use tokio::time::Instant; use tracing::trace; use zstd::{bulk::Decompressor, dict::DecoderDictionary}; -use crate::{ - json_loader::JSONLoader, - modules::{console, crypto::SYSTEM_RANDOM, path::dirname}, -}; - use crate::{ bytecode::{BYTECODE_COMPRESSED, BYTECODE_UNCOMPRESSED, BYTECODE_VERSION, SIGNATURE_LENGTH}, + custom_resolver::{require_resolve, CustomResolver}, environment, json::{parse::json_parse, stringify::json_stringify_replacer_space}, + modules::{console, crypto::SYSTEM_RANDOM}, number::number_to_string, security, - utils::{clone::structured_clone, io::get_js_path}, + utils::clone::structured_clone, }; include!(concat!(env!("OUT_DIR"), "/bytecode_cache.rs")); @@ -78,105 +72,6 @@ fn print(value: String, stdout: Opt) { } } -#[derive(Debug)] -pub struct BinaryResolver { - paths: Vec, - cwd: PathBuf, -} -impl BinaryResolver { - pub fn add_path>(&mut self, path: P) -> &mut Self { - self.paths.push(path.into()); - self - } - - pub fn get_bin_path(path: &Path) -> PathBuf { - path.with_extension("lrt") - } - - fn new() -> io::Result { - Ok(Self { - paths: Vec::with_capacity(10), - cwd: env::current_dir()?, - }) - } -} - -#[allow(clippy::manual_strip)] -impl Resolver for BinaryResolver { - fn resolve(&mut self, _ctx: &Ctx, base: &str, name: &str) -> Result { - trace!("Try resolve \"{}\" from \"{}\"", name, base); - - if BYTECODE_CACHE.contains_key(name) { - return Ok(name.to_string()); - } - - let base_path = Path::new(base); - let base_path = if base_path.is_dir() { - if base_path == self.cwd { - Path::new(".") - } else { - base_path - } - } else { - base_path.parent().unwrap_or(base_path) - }; - - let normalized_path = base_path.join(name); - let normalized_path = normalized_path.to_string_lossy().to_string(); - let normalized_path = join_path_with_separator([normalized_path].iter(), true); - let mut normalized_path = normalized_path.as_str(); - let cache_path = if normalized_path.starts_with("./") { - &normalized_path[2..] - } else { - normalized_path - }; - - let cache_key = Path::new(cache_path).with_extension("js"); - let cache_key = cache_key.to_str().unwrap(); - - trace!("Normalized path: {}, key: {}", normalized_path, cache_key); - - if BYTECODE_CACHE.contains_key(cache_key) { - return Ok(cache_key.to_string()); - } - - if BYTECODE_CACHE.contains_key(base) { - normalized_path = name; - if Path::new(name).exists() { - return Ok(name.to_string()); - } - } - - if Path::new(normalized_path).exists() { - return Ok(normalized_path.to_string()); - } - - let path = self - .paths - .iter() - .find_map(|path| { - let path = path.join(normalized_path); - let bin_path = BinaryResolver::get_bin_path(&path); - if bin_path.exists() { - return Some(bin_path); - } - get_js_path(path.to_str().unwrap()) - }) - .ok_or_else(|| Error::new_resolving(base, name))?; - - Ok(path.into_os_string().into_string().unwrap()) - } -} - -#[derive(Debug)] -pub struct BinaryLoader; - -impl Default for BinaryLoader { - fn default() -> Self { - Self - } -} - struct LoaderContainer where T: Loader + 'static, @@ -215,9 +110,17 @@ where } } -impl Loader for BinaryLoader { +#[derive(Debug, Default)] +pub struct CustomLoader; + +impl Loader for CustomLoader { fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> Result> { trace!("Loading module: {}", name); + if name.ends_with(".json") { + let source = std::fs::read_to_string(name)?; + return Module::declare(ctx.clone(), name, ["export default ", &source].concat()); + } + let ctx = ctx.clone(); if let Some(bytes) = BYTECODE_CACHE.get(name) { trace!("Loading embedded module: {}", name); @@ -342,7 +245,7 @@ impl Vm { .expect("Failed to initialize SystemRandom"); let mut file_resolver = FileResolver::default(); - let mut binary_resolver = BinaryResolver::new()?; + let custom_resolver = CustomResolver; let mut paths: Vec<&str> = Vec::with_capacity(10); paths.push("."); @@ -361,18 +264,16 @@ impl Vm { for path in paths.iter() { file_resolver.add_path(*path); - binary_resolver.add_path(*path); } let (builtin_resolver, module_loader, module_names, init_globals) = vm_options.module_builder.build(); - let resolver = (builtin_resolver, binary_resolver, file_resolver); + let resolver = (builtin_resolver, custom_resolver, file_resolver); let loader = LoaderContainer::new(( module_loader, - JSONLoader, - BinaryLoader, + CustomLoader, BuiltinLoader::default(), ScriptLoader::default() .with_extension("mjs") @@ -572,26 +473,12 @@ fn init(ctx: &Ctx<'_>, module_names: HashSet<&'static str>) -> Result<()> { } else { specifier }; - let import_name = if module_names.contains(specifier.as_str()) - || BYTECODE_CACHE.contains_key(&specifier) - || is_absolute(&specifier) - { + let import_name = if module_names.contains(specifier.as_str()) { specifier } else { let module_name = get_script_or_module_name(ctx.clone()); let abs_path = resolve_path([module_name].iter()); - let import_directory = dirname(abs_path); - let ads_specifier = if specifier.ends_with(".js") - || specifier.ends_with(".mjs") - || specifier.ends_with(".cjs") - { - specifier - } else if specifier.starts_with("./") || specifier.starts_with("../") { - [&import_directory, "/", &specifier].concat() - } else { - get_module_path_in_node_modules(&ctx, &specifier)? - }; - resolve_path([import_directory, ads_specifier].iter()) + require_resolve(&ctx, &specifier, &abs_path, false)? }; if import_name.ends_with(".json") { @@ -712,80 +599,3 @@ fn set_import_meta(module: &Module<'_>, filepath: &str) -> Result<()> { meta.prop("url", ["file://", filepath].concat())?; Ok(()) } - -fn get_module_path_in_node_modules(ctx: &Ctx<'_>, specifier: &str) -> Result { - let current_dir = env::current_dir() - .ok() - .and_then(|path| path.into_os_string().into_string().ok()) - .unwrap_or_else(|| "/".to_string()); - - let (scope, name) = match specifier.split_once('/') { - Some((s, n)) => (s, ["./", n].concat()), - None => (specifier, ".".to_string()), - }; - - let mut package_json_path = [¤t_dir, "/node_modules/"].concat(); - let base_path_length = package_json_path.len(); - package_json_path.push_str(scope); - package_json_path.push_str("/package.json"); - - let (scope, name) = if name != "." && !Path::new(&package_json_path).exists() { - package_json_path.truncate(base_path_length); - package_json_path.push_str(specifier); - package_json_path.push_str("/package.json"); - (specifier, ".") - } else { - (scope, name.as_str()) - }; - - if !Path::new(&package_json_path).exists() { - return Err(Exception::throw_reference( - ctx, - &["Error resolving module '", specifier, "'"].concat(), - )); - }; - - let mut package_json = fs::read(&package_json_path).unwrap_or_default(); - let package_json = simd_json::to_borrowed_value(&mut package_json).or_throw(ctx)?; - - let module_path = get_module_path(&package_json, name)?; - - Ok([¤t_dir, "/node_modules/", scope, "/", module_path].concat()) -} - -fn get_module_path<'a>(package_json: &'a BorrowedValue<'a>, module_name: &str) -> Result<&'a str> { - if let BorrowedValue::Object(map) = package_json { - if let Some(BorrowedValue::Object(exports)) = map.get("exports") { - if let Some(BorrowedValue::Object(name)) = exports.get(module_name) { - if let Some(BorrowedValue::Object(require)) = name.get("require") { - // Check for exports -> name -> require -> default - if let Some(BorrowedValue::String(default)) = require.get("default") { - return Ok(default.as_ref()); - } - } - // Check for exports -> name -> require - if let Some(BorrowedValue::String(require)) = name.get("require") { - return Ok(require.as_ref()); - } - } - - if let Some(BorrowedValue::Object(require)) = exports.get("require") { - // Check for exports -> require -> default - if let Some(BorrowedValue::String(default)) = require.get("default") { - return Ok(default.as_ref()); - } - } - - // Check for exports -> default - if let Some(BorrowedValue::String(default)) = exports.get("default") { - return Ok(default.as_ref()); - } - } - - // Check for main field - if let Some(BorrowedValue::String(main)) = map.get("main") { - return Ok(main.as_ref()); - } - } - Ok("./index.js") -} diff --git a/llrt_modules/src/modules/path.rs b/llrt_modules/src/modules/path.rs index 7a6fc24118..2595696d46 100644 --- a/llrt_modules/src/modules/path.rs +++ b/llrt_modules/src/modules/path.rs @@ -259,7 +259,7 @@ where { let cwd = std::env::current_dir().expect("Unable to access working directory"); - let mut result = cwd.to_string_lossy().to_string(); + let mut result = cwd.clone().into_os_string().into_string().unwrap(); //add MAIN_SEPARATOR if we're not on already MAIN_SEPARATOR if !result.ends_with(MAIN_SEPARATOR) { result.push(MAIN_SEPARATOR); @@ -467,7 +467,7 @@ fn get_path_prefix(cwd: &Path) -> (String, std::iter::Peekable String { +pub fn normalize(path: String) -> String { join_path([path].iter()) }