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 test_reporter: Option<Box<dyn Fn(TestEvent)>>,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
17#[serde(tag = "kind", rename_all = "camelCase")]
18pub enum TestEvent {
19 Passed {
20 module: String,
21 name: String,
22 },
23 Failed {
24 module: String,
25 name: String,
26 message: String,
27 },
28 Finished {
29 passed: usize,
30 failed: usize,
31 },
32}
33
34#[op2]
35fn op_alder_test_report(state: &mut OpState, #[serde] event: TestEvent) -> bool {
36 let host = state.borrow::<Rc<RefCell<HostState>>>().borrow();
37 if let Some(report) = &host.test_reporter {
38 report(event);
39 true
40 } else {
41 false
42 }
43}
44
45#[op2(fast)]
46fn op_alder_print(#[string] text: &str, stderr: bool) {
47 if stderr {
48 eprintln!("{text}");
49 } else {
50 println!("{text}");
51 }
52}
53
54#[op2]
55#[serde]
56fn op_alder_args(state: &mut OpState) -> Vec<String> {
57 state
58 .borrow::<Rc<RefCell<HostState>>>()
59 .borrow()
60 .args
61 .clone()
62}
63
64#[op2(fast)]
65fn op_alder_exit(state: &mut OpState, code: i32) {
66 state
67 .borrow::<Rc<RefCell<HostState>>>()
68 .borrow_mut()
69 .exit_code = code;
70}
71
72#[op2(async(deferred), fast)]
73async fn op_alder_sleep(#[number] milliseconds: u64) {
74 tokio::time::sleep(Duration::from_millis(milliseconds)).await;
75}
76
77deno_core::extension!(
78 alder_host,
79 deps = [
80 deno_webidl,
81 deno_web,
82 deno_fs,
83 deno_fetch,
84 deno_crypto,
85 deno_net,
86 deno_websocket,
87 deno_http
88 ],
89 ops = [op_alder_print, op_alder_args, op_alder_exit, op_alder_sleep, op_alder_test_report],
90 esm_entry_point = "ext:alder_host/bootstrap.js",
91 esm = [dir "js", "bootstrap.js"],
92);
93
94const BOOTSTRAP: &str = r#"
95const core = Deno.core;
96const render = (value) => typeof value === "string" ? value :
97 value instanceof Error ? (value.stack ?? value.message) :
98 (() => { try { return JSON.stringify(value); } catch { return String(value); } })();
99globalThis.console = {
100 log: (...values) => core.ops.op_alder_print(values.map(render).join(" "), false),
101 error: (...values) => core.ops.op_alder_print(values.map(render).join(" "), true),
102};
103globalThis.setTimeout = (callback, milliseconds = 0, ...args) => {
104 let cancelled = false;
105 core.ops.op_alder_sleep(milliseconds).then(() => { if (!cancelled) callback(...args); });
106 return { cancel: () => { cancelled = true; } };
107};
108globalThis.clearTimeout = (timer) => timer?.cancel?.();
109Object.defineProperty(globalThis, "__alderHost", {
110 value: Object.freeze({
111 args: core.ops.op_alder_args(),
112 exit: (code) => core.ops.op_alder_exit(code),
113 reportTest: (event) => core.ops.op_alder_test_report(event),
114 }),
115 enumerable: false,
116 configurable: false,
117 writable: false,
118});
119"#;
120
121pub async fn execute(bundle: String, args: Vec<String>) -> Result<i32, deno_core::error::AnyError> {
122 execute_inner(bundle, args, None).await
123}
124
125pub async fn execute_tests(
127 bundle: String,
128 report: impl Fn(TestEvent) + 'static,
129) -> Result<i32, deno_core::error::AnyError> {
130 execute_inner(bundle, Vec::new(), Some(Box::new(report))).await
131}
132
133async fn execute_inner(
134 bundle: String,
135 args: Vec<String>,
136 test_reporter: Option<Box<dyn Fn(TestEvent)>>,
137) -> Result<i32, deno_core::error::AnyError> {
138 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
142 let state = Rc::new(RefCell::new(HostState {
143 args,
144 exit_code: 0,
145 test_reporter,
146 }));
147 let host_state = state.clone();
148 let mut runtime = JsRuntime::new(RuntimeOptions {
149 extensions: vec![
150 deno_webidl::deno_webidl::init(),
151 deno_web::deno_web::init(
152 Arc::new(deno_web::BlobStore::default()),
153 None,
154 false,
155 deno_web::InMemoryBroadcastChannel::default(),
156 ),
157 deno_fs::deno_fs::init(Rc::new(deno_fs::RealFs)),
158 deno_fetch::deno_fetch::init(Default::default()),
159 deno_crypto::deno_crypto::init(None),
160 deno_net::deno_net::init(None, None),
161 deno_websocket::deno_websocket::init(),
162 deno_http::deno_http::init(Default::default()),
163 alder_host::init(),
164 ],
165 ..Default::default()
166 });
167 {
168 let op_state_handle = runtime.op_state();
169 let mut op_state = op_state_handle.borrow_mut();
170 op_state.put(host_state);
171 op_state.put(PermissionsContainer::allow_all(Arc::new(
172 RuntimePermissionDescriptorParser::new(sys_traits::impls::RealSys),
173 )));
174 op_state.put(Arc::new(deno_features::FeatureChecker::default()));
175 }
176 runtime.execute_script("alder:bootstrap", BOOTSTRAP)?;
177 let specifier = ModuleSpecifier::parse("alder://bundle/main.mjs")?;
178 let module = runtime
179 .load_main_es_module_from_code(&specifier, ModuleCodeString::from(bundle))
180 .await?;
181 let evaluation = runtime.mod_evaluate(module);
182 runtime.run_event_loop(Default::default()).await?;
183 evaluation.await?;
184 let exit_code = state.borrow().exit_code;
185 Ok(exit_code)
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[tokio::test(flavor = "current_thread")]
193 async fn executes_esm_and_records_exit_code() {
194 let code =
195 "globalThis.__alderHost.exit(__alderHost.args.length); export default 0;".to_owned();
196 assert_eq!(execute(code, vec!["one".to_owned()]).await.unwrap(), 1);
197 }
198
199 #[tokio::test(flavor = "current_thread")]
200 async fn structured_test_reporting_is_opt_in_and_preserves_exit_status() {
201 let code = "const handled = __alderHost.reportTest({kind: 'finished', passed: 2, failed: 1}); __alderHost.exit(handled ? 7 : 3);".to_owned();
202 assert_eq!(execute(code.clone(), Vec::new()).await.unwrap(), 3);
203 let events = Rc::new(RefCell::new(Vec::new()));
204 let received = events.clone();
205 assert_eq!(
206 execute_tests(code, move |event| received.borrow_mut().push(event))
207 .await
208 .unwrap(),
209 7
210 );
211 assert_eq!(
212 *events.borrow(),
213 vec![TestEvent::Finished {
214 passed: 2,
215 failed: 1
216 }]
217 );
218 }
219
220 #[tokio::test(flavor = "current_thread")]
221 async fn installs_web_fetch_url_and_crypto_globals() {
222 let code = r#"
223const response = await fetch("data:text/plain,web-ok");
224const valid = await response.text() === "web-ok"
225 && new URL("https://example.com/path").hostname === "example.com"
226 && typeof crypto.randomUUID() === "string";
227__alderHost.exit(valid ? 0 : 1);
228"#
229 .to_owned();
230 assert_eq!(execute(code, Vec::new()).await.unwrap(), 0);
231 }
232}