Skip to main content

alder_runtime/
lib.rs

1//! A small, capability-oriented host around `deno_core`.
2
3use 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/// Results from the test runner, separate from the program's console streams.
16#[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
125/// Execute a test bundle with structured results. User stdout/stderr are unchanged.
126pub 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    // Workspace consumers may enable more than one rustls provider through
139    // unrelated dependencies. Select the provider used by Deno's pinned TLS
140    // stack explicitly so fetch never relies on feature inference.
141    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}