1use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
4
5use deno_core::{JsRuntime, ModuleCodeString, ModuleSpecifier, OpState, RuntimeOptions, op2};
6use deno_permissions::{PermissionsContainer, RuntimePermissionDescriptorParser};
7
8#[derive(Default)]
9struct HostState {
10 args: Vec<String>,
11 exit_code: i32,
12}
13
14#[op2(fast)]
15fn op_alder_print(#[string] text: &str, stderr: bool) {
16 if stderr {
17 eprintln!("{text}");
18 } else {
19 println!("{text}");
20 }
21}
22
23#[op2]
24#[serde]
25fn op_alder_args(state: &mut OpState) -> Vec<String> {
26 state
27 .borrow::<Rc<RefCell<HostState>>>()
28 .borrow()
29 .args
30 .clone()
31}
32
33#[op2(fast)]
34fn op_alder_exit(state: &mut OpState, code: i32) {
35 state
36 .borrow::<Rc<RefCell<HostState>>>()
37 .borrow_mut()
38 .exit_code = code;
39}
40
41#[op2(async(deferred), fast)]
42async fn op_alder_sleep(#[number] milliseconds: u64) {
43 tokio::time::sleep(Duration::from_millis(milliseconds)).await;
44}
45
46deno_core::extension!(
47 alder_host,
48 deps = [
49 deno_webidl,
50 deno_web,
51 deno_fs,
52 deno_fetch,
53 deno_crypto,
54 deno_net,
55 deno_websocket,
56 deno_http
57 ],
58 ops = [op_alder_print, op_alder_args, op_alder_exit, op_alder_sleep],
59 esm_entry_point = "ext:alder_host/bootstrap.js",
60 esm = [dir "js", "bootstrap.js"],
61);
62
63const BOOTSTRAP: &str = r#"
64const core = Deno.core;
65const render = (value) => typeof value === "string" ? value :
66 value instanceof Error ? (value.stack ?? value.message) :
67 (() => { try { return JSON.stringify(value); } catch { return String(value); } })();
68globalThis.console = {
69 log: (...values) => core.ops.op_alder_print(values.map(render).join(" "), false),
70 error: (...values) => core.ops.op_alder_print(values.map(render).join(" "), true),
71};
72globalThis.setTimeout = (callback, milliseconds = 0, ...args) => {
73 let cancelled = false;
74 core.opAsync("op_alder_sleep", milliseconds).then(() => { if (!cancelled) callback(...args); });
75 return { cancel: () => { cancelled = true; } };
76};
77globalThis.clearTimeout = (timer) => timer?.cancel?.();
78Object.defineProperty(globalThis, "__alderHost", {
79 value: Object.freeze({
80 args: core.ops.op_alder_args(),
81 exit: (code) => core.ops.op_alder_exit(code),
82 }),
83 enumerable: false,
84 configurable: false,
85 writable: false,
86});
87"#;
88
89pub async fn execute(bundle: String, args: Vec<String>) -> Result<i32, deno_core::error::AnyError> {
90 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
94 let state = Rc::new(RefCell::new(HostState { args, exit_code: 0 }));
95 let host_state = state.clone();
96 let mut runtime = JsRuntime::new(RuntimeOptions {
97 extensions: vec![
98 deno_webidl::deno_webidl::init(),
99 deno_web::deno_web::init(
100 Arc::new(deno_web::BlobStore::default()),
101 None,
102 false,
103 deno_web::InMemoryBroadcastChannel::default(),
104 ),
105 deno_fs::deno_fs::init(Rc::new(deno_fs::RealFs)),
106 deno_fetch::deno_fetch::init(Default::default()),
107 deno_crypto::deno_crypto::init(None),
108 deno_net::deno_net::init(None, None),
109 deno_websocket::deno_websocket::init(),
110 deno_http::deno_http::init(Default::default()),
111 alder_host::init(),
112 ],
113 ..Default::default()
114 });
115 {
116 let op_state_handle = runtime.op_state();
117 let mut op_state = op_state_handle.borrow_mut();
118 op_state.put(host_state);
119 op_state.put(PermissionsContainer::allow_all(Arc::new(
120 RuntimePermissionDescriptorParser::new(sys_traits::impls::RealSys),
121 )));
122 op_state.put(Arc::new(deno_features::FeatureChecker::default()));
123 }
124 runtime.execute_script("alder:bootstrap", BOOTSTRAP)?;
125 let specifier = ModuleSpecifier::parse("alder://bundle/main.mjs")?;
126 let module = runtime
127 .load_main_es_module_from_code(&specifier, ModuleCodeString::from(bundle))
128 .await?;
129 let evaluation = runtime.mod_evaluate(module);
130 runtime.run_event_loop(Default::default()).await?;
131 evaluation.await?;
132 let exit_code = state.borrow().exit_code;
133 Ok(exit_code)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[tokio::test(flavor = "current_thread")]
141 async fn executes_esm_and_records_exit_code() {
142 let code =
143 "globalThis.__alderHost.exit(__alderHost.args.length); export default 0;".to_owned();
144 assert_eq!(execute(code, vec!["one".to_owned()]).await.unwrap(), 1);
145 }
146
147 #[tokio::test(flavor = "current_thread")]
148 async fn installs_web_fetch_url_and_crypto_globals() {
149 let code = r#"
150const response = await fetch("data:text/plain,web-ok");
151const valid = await response.text() === "web-ok"
152 && new URL("https://example.com/path").hostname === "example.com"
153 && typeof crypto.randomUUID() === "string";
154__alderHost.exit(valid ? 0 : 1);
155"#
156 .to_owned();
157 assert_eq!(execute(code, Vec::new()).await.unwrap(), 0);
158 }
159}