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}
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    // Workspace consumers may enable more than one rustls provider through
91    // unrelated dependencies. Select the provider used by Deno's pinned TLS
92    // stack explicitly so fetch never relies on feature inference.
93    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}