Skip to content

Commit

Permalink
fix: CJS export default (#673)
Browse files Browse the repository at this point in the history
* Fix CJS export default

* Clippy

* Enable back CJS named export from ESM import

* Disable test on windows

* Force CJS imports for node_modules without specified type or file ext
  • Loading branch information
richarddavison authored Nov 11, 2024
1 parent 0a7509d commit 6dcd778
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 98 deletions.
9 changes: 7 additions & 2 deletions llrt/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down Expand Up @@ -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;
}
}

Expand Down
78 changes: 51 additions & 27 deletions llrt_core/src/module_loader/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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<Vec<String>> = 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<String>)> {
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<String>)> {
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 = "`);";
Expand All @@ -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)));
}
}

Expand All @@ -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())));
}
Expand All @@ -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)))
}
}

Expand Down
3 changes: 3 additions & 0 deletions llrt_core/src/module_loader/mod.rs
Original file line number Diff line number Diff line change
@@ -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:";
64 changes: 45 additions & 19 deletions llrt_core/src/module_loader/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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);
}
}

Expand All @@ -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))
Expand All @@ -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"
Expand All @@ -194,17 +193,17 @@ fn resolved_by_bytecode_cache(x: Cow<'_, str>) -> Result<Cow<'_, str>> {

fn resolved_by_file_exists(path: Cow<'_, str>) -> Result<Cow<'_, str>> {
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<Cow<'_, str>> {
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)
Expand Down Expand Up @@ -267,7 +266,7 @@ fn load_index<'a>(ctx: &Ctx<'_>, x: Rc<String>) -> Result<Option<Cow<'a, str>>>
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.
Expand Down Expand Up @@ -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())
};

Expand All @@ -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(
Expand Down
2 changes: 0 additions & 2 deletions llrt_core/src/modules/js/@llrt/test/SocketClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion llrt_core/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 6dcd778

Please sign in to comment.