Skip to main content

obeli_sk_boa_runtime/
lib.rs

1//! Boa's **boa_runtime** crate contains an example runtime and basic runtime features and
2//! functionality for the `boa_engine` crate for runtime implementors.
3//!
4//! # Example: Adding Web API's Console Object
5//!
6//! 1. Add **boa_runtime** as a dependency to your project along with **boa_engine**.
7//!
8//! ```
9//! use boa_engine::{js_string, property::Attribute, Context, Source};
10//! use boa_runtime::Console;
11//! use boa_runtime::console::DefaultLogger;
12//!
13//! // Create the context.
14//! let mut context = Context::default();
15//!
16//! // Register the Console object to the context. The DefaultLogger simply
17//! // write errors to STDERR and all other logs to STDOUT.
18//! Console::register_with_logger(DefaultLogger, &mut context)
19//!     .expect("the console object shouldn't exist yet");
20//!
21//! // JavaScript source for parsing.
22//! let js_code = "console.log('Hello World from a JS code string!')";
23//!
24//! // Parse the source code
25//! match context.eval(Source::from_bytes(js_code)) {
26//!     Ok(res) => {
27//!         println!(
28//!             "{}",
29//!             res.to_string(&mut context).unwrap().to_std_string_escaped()
30//!         );
31//!     }
32//!     Err(e) => {
33//!         // Pretty print the error
34//!         eprintln!("Uncaught {e}");
35//!         # panic!("An error occurred in boa_runtime's js_code");
36//!     }
37//! };
38//! ```
39//!
40//! # Example: Add all supported Boa's Runtime Web API to your context
41//!
42//! ```no_run
43//! use boa_engine::{js_string, property::Attribute, Context, Source};
44//!
45//! // Create the context.
46//! let mut context = Context::default();
47//!
48//! // Register all objects in the context. To conditionally register extensions,
49//! // call `register()` directly on the extension.
50//! boa_runtime::register(
51//!     (
52//!         // Register the default logger.
53//!         boa_runtime::extensions::ConsoleExtension::default(),
54//!         // A fetcher can be added if the `fetch` feature flag is enabled.
55//!         // This fetcher uses the Reqwest blocking API to allow fetching using HTTP.
56//! #       #[cfg(feature = "reqwest-blocking")]
57//!         boa_runtime::extensions::FetchExtension(
58//!             boa_runtime::fetch::BlockingReqwestFetcher::default()
59//!         ),
60//!     ),
61//!     None,
62//!     &mut context,
63//! );
64//!
65//! // JavaScript source for parsing.
66//! let js_code = r#"
67//!     fetch("https://google.com/")
68//!         .then(response => response.text())
69//!         .then(html => console.log(html))
70//! "#;
71//!
72//! // Parse the source code
73//! match context.eval(Source::from_bytes(js_code)) {
74//!     Ok(res) => {
75//!         // The result is a promise, so we need to await it.
76//!         res
77//!             .as_promise()
78//!             .expect("Should be a promise")
79//!             .await_blocking(&mut context)
80//!             .expect("Should resolve()");
81//!         println!(
82//!             "{}",
83//!             res.to_string(&mut context).unwrap().to_std_string_escaped()
84//!         );
85//!     }
86//!     Err(e) => {
87//!         // Pretty print the error
88//!         eprintln!("Uncaught {e}");
89//!         # panic!("An error occurred in boa_runtime's js_code");
90//!     }
91//! };
92//! ```
93#![doc = include_str!("../ABOUT.md")]
94#![doc(
95    html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg",
96    html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg"
97)]
98#![cfg_attr(test, allow(clippy::needless_raw_string_hashes))] // Makes strings a bit more copy-pastable
99#![cfg_attr(not(test), forbid(clippy::unwrap_used))]
100// Currently throws a false positive regarding dependencies that are only used in tests.
101#![allow(unused_crate_dependencies)]
102#![allow(
103    clippy::module_name_repetitions,
104    clippy::redundant_pub_crate,
105    clippy::let_unit_value
106)]
107
108pub mod base64;
109pub mod console;
110
111#[doc(inline)]
112pub use console::{Console, ConsoleState, DefaultLogger, Logger, NullLogger};
113
114#[cfg(feature = "fetch")]
115pub mod abort;
116pub mod clone;
117pub mod extensions;
118#[cfg(feature = "fetch")]
119pub mod fetch;
120pub mod interval;
121pub mod message;
122pub mod microtask;
123#[cfg(feature = "process")]
124pub mod process;
125pub mod store;
126/// Support for the `$262` test262 harness object.
127#[cfg(feature = "test262")]
128pub mod test262;
129pub mod text;
130#[cfg(feature = "url")]
131pub mod url;
132
133#[cfg(feature = "process")]
134use crate::extensions::ProcessExtension;
135use crate::extensions::{
136    Base64Extension, EncodingExtension, MicrotaskExtension, StructuredCloneExtension,
137    TimeoutExtension,
138};
139pub use extensions::RuntimeExtension;
140
141/// Register all the built-in objects and functions of the `WebAPI` runtime, plus
142/// any extensions defined.
143///
144/// # Errors
145/// This will error if any of the built-in objects or functions cannot be registered.
146pub fn register(
147    extensions: impl RuntimeExtension,
148    realm: Option<boa_engine::realm::Realm>,
149    ctx: &mut boa_engine::Context,
150) -> boa_engine::JsResult<()> {
151    (
152        Base64Extension,
153        TimeoutExtension,
154        EncodingExtension,
155        MicrotaskExtension,
156        StructuredCloneExtension,
157        #[cfg(feature = "url")]
158        extensions::UrlExtension,
159        #[cfg(feature = "process")]
160        ProcessExtension,
161        #[cfg(feature = "fetch")]
162        extensions::AbortControllerExtension,
163        extensions,
164    )
165        .register(realm, ctx)?;
166
167    Ok(())
168}
169
170/// Register only the extensions provided. An application can use this to register
171/// extensions that it previously hadn't registered.
172///
173/// # Errors
174/// This will error if any of the built-in objects or functions cannot be registered.
175pub fn register_extensions(
176    extensions: impl RuntimeExtension,
177    realm: Option<boa_engine::realm::Realm>,
178    ctx: &mut boa_engine::Context,
179) -> boa_engine::JsResult<()> {
180    extensions.register(realm, ctx)?;
181
182    Ok(())
183}
184
185#[cfg(test)]
186pub(crate) mod test {
187    use crate::extensions::ConsoleExtension;
188    use crate::register;
189    use boa_engine::{Context, JsError, JsResult, JsValue, Source, builtins};
190    use std::borrow::Cow;
191    use std::path::{Path, PathBuf};
192    use std::pin::Pin;
193
194    /// A test action executed in a test function.
195    #[allow(missing_debug_implementations)]
196    pub(crate) struct TestAction(Inner);
197
198    #[allow(dead_code)]
199    #[allow(clippy::type_complexity)]
200    enum Inner {
201        RunHarness,
202        Run {
203            source: Cow<'static, str>,
204        },
205        RunFile {
206            path: PathBuf,
207        },
208        RunJobs,
209        InspectContext {
210            op: Box<dyn FnOnce(&mut Context)>,
211        },
212        InspectContextAsync {
213            op: Box<dyn for<'a> FnOnce(&'a mut Context) -> Pin<Box<dyn Future<Output = ()> + 'a>>>,
214        },
215        Assert {
216            source: Cow<'static, str>,
217        },
218        AssertEq {
219            source: Cow<'static, str>,
220            expected: JsValue,
221        },
222        AssertWithOp {
223            source: Cow<'static, str>,
224            op: fn(JsValue, &mut Context) -> bool,
225        },
226        AssertOpaqueError {
227            source: Cow<'static, str>,
228            expected: JsValue,
229        },
230        AssertNativeError {
231            source: Cow<'static, str>,
232            kind: builtins::error::ErrorKind,
233            message: &'static str,
234        },
235        AssertContext {
236            op: fn(&mut Context) -> bool,
237        },
238    }
239
240    impl TestAction {
241        #[allow(unused)]
242        pub(crate) fn harness() -> Self {
243            Self(Inner::RunHarness)
244        }
245
246        /// Runs `source`, panicking if the execution throws.
247        pub(crate) fn run(source: impl Into<Cow<'static, str>>) -> Self {
248            Self(Inner::Run {
249                source: source.into(),
250            })
251        }
252
253        /// Executes `op` with the currently active context.
254        ///
255        /// Useful to make custom assertions that must be done from Rust code.
256        pub(crate) fn inspect_context(op: impl FnOnce(&mut Context) + 'static) -> Self {
257            Self(Inner::InspectContext { op: Box::new(op) })
258        }
259
260        /// Executes `op` with the currently active context in an async environment.
261        pub(crate) fn inspect_context_async(op: impl AsyncFnOnce(&mut Context) + 'static) -> Self {
262            Self(Inner::InspectContextAsync {
263                op: Box::new(move |ctx| Box::pin(op(ctx))),
264            })
265        }
266    }
267
268    /// Executes a list of test actions on a new, default context.
269    #[track_caller]
270    pub(crate) fn run_test_actions(actions: impl IntoIterator<Item = TestAction>) {
271        let context = &mut Context::default();
272        register(ConsoleExtension::default(), None, context)
273            .expect("failed to register WebAPI objects");
274        run_test_actions_with(actions, context);
275    }
276
277    /// Executes a list of test actions on the provided context.
278    #[track_caller]
279    #[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
280    pub(crate) fn run_test_actions_with(
281        actions: impl IntoIterator<Item = TestAction>,
282        context: &mut Context,
283    ) {
284        #[track_caller]
285        fn forward_val(context: &mut Context, source: &str) -> JsResult<JsValue> {
286            context.eval(Source::from_bytes(source))
287        }
288
289        #[track_caller]
290        fn forward_file(context: &mut Context, path: impl AsRef<Path>) -> JsResult<JsValue> {
291            let p = path.as_ref();
292            context.eval(Source::from_filepath(p).map_err(JsError::from_rust)?)
293        }
294
295        #[track_caller]
296        fn fmt_test(source: &str, test: usize) -> String {
297            format!(
298                "\n\nTest case {test}: \n```\n{}\n```",
299                textwrap::indent(source, "    ")
300            )
301        }
302
303        // Some unwrapping patterns look weird because they're replaceable
304        // by simpler patterns like `unwrap_or_else` or `unwrap_err
305        let mut i = 1;
306        for action in actions.into_iter().map(|a| a.0) {
307            match action {
308                Inner::RunHarness => {
309                    if let Err(e) = forward_file(context, "./assets/harness.js") {
310                        panic!("Uncaught {e} in the test harness");
311                    }
312                }
313                Inner::Run { source } => {
314                    if let Err(e) = forward_val(context, &source) {
315                        panic!("{}\nUncaught {e}", fmt_test(&source, i));
316                    }
317                }
318                Inner::RunFile { path } => {
319                    if let Err(e) = forward_file(context, &path) {
320                        panic!("Uncaught {e} in file {path:?}");
321                    }
322                }
323                Inner::RunJobs => {
324                    if let Err(e) = context.run_jobs() {
325                        panic!("Uncaught {e} in a job");
326                    }
327                }
328                Inner::InspectContext { op } => {
329                    op(context);
330                }
331                Inner::InspectContextAsync { op } => futures_lite::future::block_on(op(context)),
332                Inner::Assert { source } => {
333                    let val = match forward_val(context, &source) {
334                        Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
335                        Ok(v) => v,
336                    };
337                    let Some(val) = val.as_boolean() else {
338                        panic!(
339                            "{}\nTried to assert with the non-boolean value `{}`",
340                            fmt_test(&source, i),
341                            val.display()
342                        )
343                    };
344                    assert!(val, "{}", fmt_test(&source, i));
345                    i += 1;
346                }
347                Inner::AssertEq { source, expected } => {
348                    let val = match forward_val(context, &source) {
349                        Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
350                        Ok(v) => v,
351                    };
352                    assert_eq!(val, expected, "{}", fmt_test(&source, i));
353                    i += 1;
354                }
355                Inner::AssertWithOp { source, op } => {
356                    let val = match forward_val(context, &source) {
357                        Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
358                        Ok(v) => v,
359                    };
360                    assert!(op(val, context), "{}", fmt_test(&source, i));
361                    i += 1;
362                }
363                Inner::AssertOpaqueError { source, expected } => {
364                    let err = match forward_val(context, &source) {
365                        Ok(v) => panic!(
366                            "{}\nExpected error, got value `{}`",
367                            fmt_test(&source, i),
368                            v.display()
369                        ),
370                        Err(e) => e,
371                    };
372                    let Some(err) = err.as_opaque() else {
373                        panic!(
374                            "{}\nExpected opaque error, got native error `{}`",
375                            fmt_test(&source, i),
376                            err
377                        )
378                    };
379
380                    assert_eq!(err, &expected, "{}", fmt_test(&source, i));
381                    i += 1;
382                }
383                Inner::AssertNativeError {
384                    source,
385                    kind,
386                    message,
387                } => {
388                    let err = match forward_val(context, &source) {
389                        Ok(v) => panic!(
390                            "{}\nExpected error, got value `{}`",
391                            fmt_test(&source, i),
392                            v.display()
393                        ),
394                        Err(e) => e,
395                    };
396                    let native = match err.try_native(context) {
397                        Ok(err) => err,
398                        Err(e) => panic!(
399                            "{}\nCouldn't obtain a native error: {e}",
400                            fmt_test(&source, i)
401                        ),
402                    };
403
404                    assert_eq!(native.kind(), &kind, "{}", fmt_test(&source, i));
405                    assert_eq!(native.message(), message, "{}", fmt_test(&source, i));
406                    i += 1;
407                }
408                Inner::AssertContext { op } => {
409                    assert!(op(context), "Test case {i}");
410                    i += 1;
411                }
412            }
413        }
414    }
415}