use std::collections::HashMap;
use quickjs_oxide::engine::api::core::{
Context, JsString, ModuleImportAttributes, ModuleLoader, ModuleScope, ModuleSource,
NativeErrorKind, PromiseState, Runtime, RuntimeError, Value,
};
fn global(context: &mut Context, name: &str) -> Value {
context.eval(name).unwrap()
}
fn text(value: &Value) -> String {
match value {
Value::String(text) => text.to_utf8_lossy(),
other => panic!("expected a string, got {other:?}"),
}
}
fn run_module() {
let runtime = Runtime::new();
let context = runtime.context();
let module = context
.compile_module_with_filename(
"await Promise.resolve(); globalThis.answer = 6 * 7;",
"main.mjs",
)
.unwrap();
assert_eq!(module.name(), "main.mjs");
let Value::Object(promise) = context.execute_module(&module).unwrap() else {
panic!("expected a promise");
};
runtime.run_jobs().unwrap();
let snapshot = runtime.promise_snapshot(&promise).unwrap().unwrap();
assert_eq!(snapshot.state(), PromiseState::Fulfilled);
assert_eq!(context.eval("answer").unwrap(), Value::Int(42));
}
struct MemoryLoader {
files: HashMap<String, String>,
}
impl ModuleLoader for MemoryLoader {
fn load(
&mut self,
scope: &mut ModuleScope<'_>,
name: &str,
attributes: &ModuleImportAttributes,
) -> Result<ModuleSource, Value> {
let Some(text) = self.files.get(name) else {
let message = format!("could not load module filename '{name}'");
return Err(scope
.new_native_error(NativeErrorKind::Reference, &message)
.unwrap_or(Value::Undefined));
};
if name.ends_with(".json") || import_type_is(attributes, "json") {
return Ok(ModuleSource::Json(text.clone().into_bytes()));
}
let url = JsString::try_from_utf8(&format!("mem:///{name}")).unwrap();
Ok(ModuleSource::Source {
text: text.clone().into_bytes(),
import_meta: vec![("url".to_owned(), Value::String(url))],
})
}
}
fn import_type_is(attributes: &ModuleImportAttributes, expected: &str) -> bool {
attributes.effective().is_some_and(|attributes| {
attributes.iter().any(|attribute| {
attribute.key.to_utf8_lossy() == "type" && attribute.value.to_utf8_lossy() == expected
})
})
}
fn use_loader() {
let files = HashMap::from([
(
"lib/math.mjs".to_owned(),
"export const square = (x) => x * x; export const where = import.meta.url;".to_owned(),
),
("lib/config.json".to_owned(), r#"{ "size": 9 }"#.to_owned()),
]);
let runtime = Runtime::new();
runtime.set_module_loader(MemoryLoader { files }).unwrap();
let mut context = runtime.context();
let module = context
.compile_module_with_filename(
r#"
import { square, where } from "./math.mjs";
import config from "./config.json" with { type: "json" };
globalThis.result = square(config.size);
globalThis.where = where;
"#,
"lib/main.mjs",
)
.unwrap();
context.execute_module(&module).unwrap();
runtime.run_jobs().unwrap();
assert_eq!(global(&mut context, "result"), Value::Int(81));
assert_eq!(text(&global(&mut context, "where")), "mem:///lib/math.mjs");
}
fn missing_import() {
let runtime = Runtime::new();
runtime
.set_module_loader(MemoryLoader {
files: HashMap::new(),
})
.unwrap();
let context = runtime.context();
let module = context
.compile_module_with_filename("import './gone.mjs';", "main.mjs")
.unwrap();
let Err(RuntimeError::Exception(exception)) = context.execute_module(&module) else {
panic!("expected a ReferenceError");
};
assert!(
runtime
.print_value(&exception)
.unwrap()
.starts_with("ReferenceError: could not load module filename 'gone.mjs'")
);
}
fn dynamic_import() {
let files = HashMap::from([(
"greet.mjs".to_owned(),
"export default (name) => `hello, ${name}`;".to_owned(),
)]);
let runtime = Runtime::new();
runtime.set_module_loader(MemoryLoader { files }).unwrap();
let mut context = runtime.context();
context
.eval(
"import('greet.mjs').then((m) => { globalThis.greeting = m.default('module'); });
import('nope.mjs').catch((e) => { globalThis.failure = e.message; });",
)
.unwrap();
runtime.run_jobs().unwrap();
assert_eq!(text(&global(&mut context, "greeting")), "hello, module");
assert_eq!(
text(&global(&mut context, "failure")),
"could not load module filename 'nope.mjs'"
);
}
fn import_meta() {
let runtime = Runtime::new();
let mut context = runtime.context();
let module = context
.compile_module_bytes_with_import_meta(
b"globalThis.meta = [import.meta.url, import.meta.main].join(' ');",
"app.mjs",
vec![
(
"url".to_owned(),
Value::String(JsString::from_static("file:///app.mjs")),
),
("main".to_owned(), Value::Bool(true)),
],
)
.unwrap();
context.execute_module(&module).unwrap();
runtime.run_jobs().unwrap();
assert_eq!(text(&global(&mut context, "meta")), "file:///app.mjs true");
}
fn failures() {
let runtime = Runtime::new();
let context = runtime.context();
let Err(RuntimeError::Exception(exception)) =
context.compile_module_with_filename("export {", "bad.mjs")
else {
panic!("expected a SyntaxError");
};
assert!(
runtime
.print_value(&exception)
.unwrap()
.starts_with("SyntaxError")
);
let module = context
.compile_module_with_filename("throw new RangeError('boom');", "throws.mjs")
.unwrap();
let Value::Object(promise) = context.execute_module(&module).unwrap() else {
panic!("expected a promise");
};
runtime.run_jobs().unwrap();
let snapshot = runtime.promise_snapshot(&promise).unwrap().unwrap();
assert_eq!(snapshot.state(), PromiseState::Rejected);
let reason = runtime.print_value(snapshot.result()).unwrap();
assert!(reason.starts_with("RangeError: boom"));
}
struct StdLoader;
impl ModuleLoader for StdLoader {
fn normalize(
&mut self,
scope: &mut ModuleScope<'_>,
_referrer: &str,
specifier: &str,
_attributes: &ModuleImportAttributes,
) -> Result<String, Value> {
match specifier.strip_prefix("std:") {
Some(name) => Ok(format!("<std>/{name}.mjs")),
None => Err(scope
.new_native_error(NativeErrorKind::Type, "only std: modules")
.unwrap_or(Value::Undefined)),
}
}
fn load(
&mut self,
scope: &mut ModuleScope<'_>,
name: &str,
_attributes: &ModuleImportAttributes,
) -> Result<ModuleSource, Value> {
let text = format!(
"globalThis.evaluations = (globalThis.evaluations ?? 0) + 1;
export const name = {name:?};"
);
let module = scope.compile_module(name, text.as_bytes(), Vec::new())?;
Ok(ModuleSource::Compiled(module))
}
}
fn custom_normalize() {
let runtime = Runtime::new();
runtime.set_module_loader(StdLoader).unwrap();
let mut context = runtime.context();
let module = context
.compile_module_with_filename(
"import { name } from 'std:os'; globalThis.loaded = name;",
"main.mjs",
)
.unwrap();
context.execute_module(&module).unwrap();
runtime.run_jobs().unwrap();
assert_eq!(text(&global(&mut context, "loaded")), "<std>/os.mjs");
context
.eval("import('./x.mjs').catch((e) => { globalThis.refused = e.message; })")
.unwrap();
runtime.run_jobs().unwrap();
assert_eq!(text(&global(&mut context, "refused")), "only std: modules");
context
.eval("import('std:os').then((m) => { globalThis.again = m.name; })")
.unwrap();
runtime.run_jobs().unwrap();
assert_eq!(text(&global(&mut context, "again")), "<std>/os.mjs");
assert_eq!(global(&mut context, "evaluations"), Value::Int(1));
}
fn main() {
run_module();
custom_normalize();
use_loader();
missing_import();
dynamic_import();
import_meta();
failures();
println!("modules: ok");
}