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
8mod dev;
9pub use dev::{DevEvent, execute_dev};
10
11#[derive(Default)]
12struct HostState {
13 args: Vec<String>,
14 exit_code: i32,
15 test_reporter: Option<Box<dyn Fn(TestEvent)>>,
16 build_output: Option<Rc<RefCell<Option<String>>>>,
17}
18
19#[op2(fast)]
20fn op_alder_build_output(
21 state: &mut OpState,
22 #[string] output: String,
23) -> Result<(), deno_error::JsErrorBox> {
24 let host = state.borrow::<Rc<RefCell<HostState>>>().borrow();
25 let sink = host
26 .build_output
27 .as_ref()
28 .ok_or_else(|| deno_error::JsErrorBox::generic("No build evaluation is active"))?;
29 let mut sink = sink.borrow_mut();
30 if sink.is_some() {
31 return Err(deno_error::JsErrorBox::generic(
32 "Build evaluation reported more than one result",
33 ));
34 }
35 *sink = Some(output);
36 Ok(())
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
41#[serde(tag = "kind", rename_all = "camelCase")]
42pub enum TestEvent {
43 Passed {
44 module: String,
45 name: String,
46 },
47 Failed {
48 module: String,
49 name: String,
50 message: String,
51 },
52 Finished {
53 passed: usize,
54 failed: usize,
55 },
56}
57
58#[op2]
59fn op_alder_test_report(state: &mut OpState, #[serde] event: TestEvent) -> bool {
60 let host = state.borrow::<Rc<RefCell<HostState>>>().borrow();
61 if let Some(report) = &host.test_reporter {
62 report(event);
63 true
64 } else {
65 false
66 }
67}
68
69#[op2(fast)]
70fn op_alder_print(#[string] text: &str, stderr: bool) {
71 if stderr {
72 eprintln!("{text}");
73 } else {
74 println!("{text}");
75 }
76}
77
78#[op2]
79#[serde]
80fn op_alder_args(state: &mut OpState) -> Vec<String> {
81 state
82 .borrow::<Rc<RefCell<HostState>>>()
83 .borrow()
84 .args
85 .clone()
86}
87
88#[op2(fast)]
89fn op_alder_exit(state: &mut OpState, code: i32) {
90 state
91 .borrow::<Rc<RefCell<HostState>>>()
92 .borrow_mut()
93 .exit_code = code;
94}
95
96#[op2(async(deferred), fast)]
97async fn op_alder_sleep(#[number] milliseconds: u64) {
98 tokio::time::sleep(Duration::from_millis(milliseconds)).await;
99}
100
101deno_core::extension!(
102 alder_host,
103 deps = [
104 deno_webidl,
105 deno_web,
106 deno_fs,
107 deno_fetch,
108 deno_crypto,
109 deno_net,
110 deno_websocket,
111 deno_telemetry,
112 deno_http
113 ],
114 ops = [op_alder_print, op_alder_args, op_alder_exit, op_alder_sleep, op_alder_test_report, op_alder_build_output, dev::op_alder_dev_event],
115 esm_entry_point = "ext:alder_host/bootstrap.js",
116 esm = [dir "js", "bootstrap.js"],
117);
118
119const BOOTSTRAP: &str = r#"
120const core = Deno.core;
121const render = (value) => typeof value === "string" ? value :
122 value instanceof Error ? (value.stack ?? value.message) :
123 (() => { try { return JSON.stringify(value); } catch { return String(value); } })();
124globalThis.console = {
125 log: (...values) => core.ops.op_alder_print(values.map(render).join(" "), false),
126 error: (...values) => core.ops.op_alder_print(values.map(render).join(" "), true),
127};
128Object.defineProperty(globalThis, "__alderHost", {
129 value: Object.freeze({
130 args: core.ops.op_alder_args(),
131 exit: (code) => core.ops.op_alder_exit(code),
132 reportTest: (event) => core.ops.op_alder_test_report(event),
133 serve: globalThis.__alderServe,
134 devEvent: () => core.ops.op_alder_dev_event(),
135 reportBuild: (value) => core.ops.op_alder_build_output(value),
136 }),
137 enumerable: false,
138 configurable: false,
139 writable: false,
140});
141"#;
142
143pub async fn execute(bundle: String, args: Vec<String>) -> Result<i32, deno_core::error::AnyError> {
144 execute_inner(bundle, args, None, None, None).await
145}
146
147pub async fn execute_build(bundle: String) -> Result<String, deno_core::error::AnyError> {
149 let output = Rc::new(RefCell::new(None));
150 let status = execute_inner(bundle, Vec::new(), None, None, Some(output.clone())).await?;
151 if status != 0 {
152 return Err(deno_error::JsErrorBox::generic(format!(
153 "Build evaluation exited with {status}"
154 ))
155 .into());
156 }
157 let value = output.borrow_mut().take().ok_or_else(|| {
158 deno_error::JsErrorBox::generic("Build evaluation did not return a result")
159 })?;
160 Ok(value)
161}
162
163pub async fn execute_tests(
165 bundle: String,
166 report: impl Fn(TestEvent) + 'static,
167) -> Result<i32, deno_core::error::AnyError> {
168 execute_inner(bundle, Vec::new(), Some(Box::new(report)), None, None).await
169}
170
171async fn execute_inner(
172 bundle: String,
173 args: Vec<String>,
174 test_reporter: Option<Box<dyn Fn(TestEvent)>>,
175 dev: Option<Rc<dev::DevState>>,
176 build_output: Option<Rc<RefCell<Option<String>>>>,
177) -> Result<i32, deno_core::error::AnyError> {
178 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
182 let state = Rc::new(RefCell::new(HostState {
183 args,
184 exit_code: 0,
185 test_reporter,
186 build_output,
187 }));
188 let host_state = state.clone();
189 let mut runtime = JsRuntime::new(RuntimeOptions {
190 module_loader: dev
191 .as_ref()
192 .map(|state| state.clone() as Rc<dyn deno_core::ModuleLoader>),
193 extension_transpiler: Some(Rc::new(|name, source| {
194 if !name.ends_with(".ts") {
195 return Ok((source, None));
196 }
197 let allocator = oxc_allocator::Allocator::default();
200 let parsed =
201 oxc_parser::Parser::new(&allocator, &source, oxc_span::SourceType::ts()).parse();
202 if !parsed.errors.is_empty() {
203 return Err(deno_error::JsErrorBox::generic(format!(
204 "cannot parse runtime extension {name}: {:?}",
205 parsed.errors
206 )));
207 }
208 let mut program = parsed.program;
209 let semantic = oxc_semantic::SemanticBuilder::new().build(&program);
210 if !semantic.errors.is_empty() {
211 return Err(deno_error::JsErrorBox::generic(format!(
212 "cannot analyze runtime extension {name}: {:?}",
213 semantic.errors
214 )));
215 }
216 let options = oxc_transformer::TransformOptions::default();
217 let transformed = oxc_transformer::Transformer::new(
218 &allocator,
219 std::path::Path::new(name.as_str()),
220 &options,
221 )
222 .build_with_scoping(semantic.semantic.into_scoping(), &mut program);
223 if !transformed.errors.is_empty() {
224 return Err(deno_error::JsErrorBox::generic(format!(
225 "cannot lower runtime extension {name}: {:?}",
226 transformed.errors
227 )));
228 }
229 Ok((
230 oxc_codegen::Codegen::new().build(&program).code.into(),
231 None,
232 ))
233 })),
234 extensions: vec![
235 deno_webidl::deno_webidl::init(),
236 deno_web::deno_web::init(
237 Arc::new(deno_web::BlobStore::default()),
238 None,
239 false,
240 deno_web::InMemoryBroadcastChannel::default(),
241 ),
242 deno_fs::deno_fs::init(Rc::new(deno_fs::RealFs)),
243 deno_fetch::deno_fetch::init(Default::default()),
244 deno_crypto::deno_crypto::init(None),
245 deno_net::deno_net::init(None, None),
246 deno_websocket::deno_websocket::init(),
247 deno_telemetry::deno_telemetry::init(),
248 deno_http::deno_http::init(Default::default()),
249 alder_host::init(),
250 ],
251 ..Default::default()
252 });
253 {
254 let op_state_handle = runtime.op_state();
255 let mut op_state = op_state_handle.borrow_mut();
256 op_state.put(host_state);
257 if let Some(dev) = dev {
258 op_state.put(dev);
259 }
260 op_state.put(PermissionsContainer::allow_all(Arc::new(
261 RuntimePermissionDescriptorParser::new(sys_traits::impls::RealSys),
262 )));
263 op_state.put(Arc::new(deno_features::FeatureChecker::default()));
264 }
265 runtime.execute_script("alder:bootstrap", BOOTSTRAP)?;
266 let specifier = ModuleSpecifier::parse("alder://bundle/main.mjs")?;
267 let module = runtime
268 .load_main_es_module_from_code(&specifier, ModuleCodeString::from(bundle))
269 .await?;
270 let evaluation = runtime.mod_evaluate(module);
271 runtime.run_event_loop(Default::default()).await?;
272 evaluation.await?;
273 let exit_code = state.borrow().exit_code;
274 Ok(exit_code)
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[tokio::test(flavor = "current_thread")]
282 async fn build_report_is_structured_and_independent_of_console_output() {
283 let report = execute_build(r#"console.log("not the build result"); console.error("also not the result"); await Promise.resolve(); __alderHost.reportBuild('{"pages":["/one"]}');"#.to_owned()).await.unwrap();
284 assert_eq!(report, r#"{"pages":["/one"]}"#);
285 }
286
287 #[tokio::test(flavor = "current_thread")]
288 async fn build_report_requires_exactly_one_result_and_successful_exit() {
289 for (code, expected) in [
290 ("console.log('no result');", "did not return a result"),
291 (
292 "__alderHost.reportBuild('one'); __alderHost.reportBuild('two');",
293 "more than one result",
294 ),
295 (
296 "__alderHost.reportBuild('result'); __alderHost.exit(7);",
297 "exited with 7",
298 ),
299 (
300 "__alderHost.reportBuild('result'); throw new Error('later failure');",
301 "later failure",
302 ),
303 ] {
304 let error = execute_build(code.to_owned()).await.unwrap_err();
305 assert!(error.to_string().contains(expected), "{error}");
306 }
307 }
308
309 #[tokio::test(flavor = "current_thread")]
310 async fn build_report_sink_is_isolated_between_runs_and_disabled_otherwise() {
311 assert_eq!(
312 execute_build("__alderHost.reportBuild('first');".to_owned())
313 .await
314 .unwrap(),
315 "first"
316 );
317 assert_eq!(
318 execute_build("__alderHost.reportBuild('second');".to_owned())
319 .await
320 .unwrap(),
321 "second"
322 );
323 let error = execute("__alderHost.reportBuild('not allowed');".to_owned(), vec![])
324 .await
325 .unwrap_err();
326 assert!(
327 error.to_string().contains("No build evaluation is active"),
328 "{error}"
329 );
330 }
331
332 #[tokio::test(flavor = "current_thread")]
333 async fn executes_esm_and_records_exit_code() {
334 let code =
335 "globalThis.__alderHost.exit(__alderHost.args.length); export default 0;".to_owned();
336 assert_eq!(execute(code, vec!["one".to_owned()]).await.unwrap(), 1);
337 }
338
339 #[tokio::test(flavor = "current_thread")]
340 async fn structured_test_reporting_is_opt_in_and_preserves_exit_status() {
341 let code = "const handled = __alderHost.reportTest({kind: 'finished', passed: 2, failed: 1}); __alderHost.exit(handled ? 7 : 3);".to_owned();
342 assert_eq!(execute(code.clone(), Vec::new()).await.unwrap(), 3);
343 let events = Rc::new(RefCell::new(Vec::new()));
344 let received = events.clone();
345 assert_eq!(
346 execute_tests(code, move |event| received.borrow_mut().push(event))
347 .await
348 .unwrap(),
349 7
350 );
351 assert_eq!(
352 *events.borrow(),
353 vec![TestEvent::Finished {
354 passed: 2,
355 failed: 1
356 }]
357 );
358 }
359
360 #[tokio::test(flavor = "current_thread")]
361 async fn installs_web_fetch_url_and_crypto_globals() {
362 let code = r#"
363const response = await fetch("data:text/plain,web-ok");
364const valid = await response.text() === "web-ok"
365 && new URL("https://example.com/path").hostname === "example.com"
366 && typeof crypto.randomUUID() === "string";
367__alderHost.exit(valid ? 0 : 1);
368"#
369 .to_owned();
370 assert_eq!(execute(code, Vec::new()).await.unwrap(), 0);
371 }
372
373 #[tokio::test(flavor = "current_thread")]
374 async fn standalone_http_serves_concurrent_requests_and_releases_listener() {
375 let code = r#"
376const server = __alderHost.serve(async request => {
377 const url = new URL(request.url);
378 return new Response(await request.text() + url.pathname, {headers: {"x-alder": "http"}});
379}, {port: 0});
380const base = `http://127.0.0.1:${server.addr.port}`;
381const responses = await Promise.all(["one", "two"].map(value => fetch(base + "/" + value, {method: "POST", body: value})));
382for (let index = 0; index < responses.length; index++) {
383 const word = index === 0 ? "one" : "two";
384 if (await responses[index].text() !== word + "/" + word || responses[index].headers.get("x-alder") !== "http") throw Error("HTTP response mismatch");
385}
386await server.shutdown();
387await server.shutdown();
388const reused = __alderHost.serve(() => new Response("reused"), {port: server.addr.port});
389await reused.shutdown();
390"#.to_owned();
391 tokio::time::timeout(Duration::from_secs(10), execute(code, vec![]))
392 .await
393 .unwrap()
394 .unwrap();
395 }
396
397 #[tokio::test(flavor = "current_thread")]
398 async fn cancelled_web_timers_do_not_keep_the_runtime_alive() {
399 let code = r#"
400const timeout = setTimeout(() => {throw Error("cancelled timeout ran");}, 60000);
401const interval = setInterval(() => {throw Error("cancelled interval ran");}, 60000);
402clearTimeout(timeout); clearInterval(interval);
403await new Promise(resolve => setTimeout(resolve, 1));
404"#
405 .to_owned();
406 tokio::time::timeout(Duration::from_secs(2), execute(code, vec![]))
407 .await
408 .expect("cancelled timers release event-loop references")
409 .unwrap();
410 }
411}