rustyscript/
lib.rs

1//! ![Rustyscript - Effortless JS Integration for Rust](https://raw.githubusercontent.com/rscarson/rustyscript/refs/heads/master/.github/rustyscript-logo-wide.png)
2//!
3//! [![Crates.io](https://img.shields.io/crates/v/rustyscript.svg)](https://crates.io/crates/rustyscript/)
4//! [![Build Status](https://github.com/rscarson/rustyscript/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/rscarson/rustyscript/actions?query=branch%3Amaster)
5//! [![docs.rs](https://img.shields.io/docsrs/rustyscript)](https://docs.rs/rustyscript/latest/rustyscript/)
6//! [![Static Badge](https://img.shields.io/badge/mdbook-user%20guide-blue)](https://rscarson.github.io/rustyscript-book/)
7//! [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/rscarson/rustyscript/master/LICENSE)
8//!
9//! ## Rustyscript - Effortless JS Integration for Rust
10//!
11//! rustyscript provides a quick and simple way to integrate a runtime javascript or typescript component from within Rust.
12//!
13//! It uses the v8 engine through the `deno_core` crate, and aims to be as simple as possible to use without sacrificing flexibility or performance.  
14//! I also have attempted to abstract away the v8 engine details so you can for the most part operate directly on rust types.
15//!
16//!
17//! **Sandboxed**  
18//! By default, the code being run is entirely sandboxed from the host, having no filesystem or network access.  
19//! [extensions](https://rscarson.github.io/rustyscript-book/extensions) can be added to grant additional capabilities that may violate sandboxing
20//!
21//! **Flexible**  
22//! The runtime is designed to be as flexible as possible, allowing you to modify capabilities, the module loader, and more.  
23//! - Asynchronous JS is fully supported, and the runtime can be configured to run in a multithreaded environment.  
24//! - Typescript is supported, and will be transpired into JS for execution.
25//! - Node JS is supported experimentally, but is not yet fully compatible ([See the `NodeJS` Compatibility section](https://rscarson.github.io/rustyscript-book/advanced/nodejs_compatibility.md))
26//!
27//! **Unopinionated**  
28//! Rustyscript is designed to be a thin wrapper over the Deno runtime, to remove potential pitfalls and simplify the API without sacrificing flexibility or performance.
29//!
30//! -----
31//!
32//! Here is a very basic use of this crate to execute a JS module. It will:
33//! - Create a basic runtime
34//! - Load a javascript module,
35//! - Call a function registered as the entrypoint
36//! - Return the resulting value
37//! ```rust
38//! use rustyscript::{json_args, Runtime, Module, Error};
39//!
40//! # fn main() -> Result<(), Error> {
41//! let module = Module::new(
42//!     "test.js",
43//!     "
44//!     export default (string, integer) => {
45//!         console.log(`Hello world: string=${string}, integer=${integer}`);
46//!         return 2;
47//!     }
48//!     "
49//! );
50//!
51//! let value: usize = Runtime::execute_module(
52//!     &module, vec![],
53//!     Default::default(),
54//!     json_args!("test", 5)
55//! )?;
56//!
57//! assert_eq!(value, 2);
58//! # Ok(())
59//! # }
60//! ```
61//!
62//! Modules can also be loaded from the filesystem with [`Module::load`] or [`Module::load_dir`] if you want to collect all modules in a given directory.
63//!
64//! ----
65//!
66//! If all you need is the result of a single javascript expression, you can use:
67//! ```rust
68//! let result: i64 = rustyscript::evaluate("5 + 5").expect("The expression was invalid!");
69//! ```
70//!
71//! Or to just import a single module for use:
72//! ```no_run
73//! use rustyscript::{json_args, import};
74//! let mut module = import("js/my_module.js").expect("Something went wrong!");
75//! let value: String = module.call("exported_function_name", json_args!()).expect("Could not get a value!");
76//! ```
77//!
78//! There are a few other utilities included, such as [`validate`] and [`resolve_path`]
79//!
80//! ----
81//!
82//! A more detailed version of the crate's usage can be seen below, which breaks down the steps instead of using the one-liner [`Runtime::execute_module`]:
83//! ```rust
84//! use rustyscript::{json_args, Runtime, RuntimeOptions, Module, Error, Undefined};
85//! use std::time::Duration;
86//!
87//! # fn main() -> Result<(), Error> {
88//! let module = Module::new(
89//!     "test.js",
90//!     "
91//!     let internalValue = 0;
92//!     export const load = (value) => internalValue = value;
93//!     export const getValue = () => internalValue;
94//!     "
95//! );
96//!
97//! // Create a new runtime
98//! let mut runtime = Runtime::new(RuntimeOptions {
99//!     timeout: Duration::from_millis(50), // Stop execution by force after 50ms
100//!     default_entrypoint: Some("load".to_string()), // Run this as the entrypoint function if none is registered
101//!     ..Default::default()
102//! })?;
103//!
104//! // The handle returned is used to get exported functions and values from that module.
105//! // We then call the entrypoint function, but do not need a return value.
106//! //Load can be called multiple times, and modules can import other loaded modules
107//! // Using `import './filename.js'`
108//! let module_handle = runtime.load_module(&module)?;
109//! runtime.call_entrypoint::<Undefined>(&module_handle, json_args!(2))?;
110//!
111//! // Functions don't need to be the entrypoint to be callable!
112//! let internal_value: i64 = runtime.call_function(Some(&module_handle), "getValue", json_args!())?;
113//! # Ok(())
114//! # }
115//! ```
116//!
117//! There are also '_async' and 'immediate' versions of most runtime functions;
118//! '_async' functions return a future that resolves to the result of the operation, while
119//! '_immediate' functions will make no attempt to wait for the event loop, making them suitable
120//! for using [`crate::js_value::Promise`]
121//!
122//! Rust functions can also be registered to be called from javascript:
123//! ```rust
124//! use rustyscript::{ Runtime, Module, serde_json::Value };
125//!
126//! # fn main() -> Result<(), rustyscript::Error> {
127//! let module = Module::new("test.js", " rustyscript.functions.foo(); ");
128//! let mut runtime = Runtime::new(Default::default())?;
129//! runtime.register_function("foo", |args| {
130//!     if let Some(value) = args.get(0) {
131//!         println!("called with: {}", value);
132//!     }
133//!     Ok(Value::Null)
134//! })?;
135//! runtime.load_module(&module)?;
136//! # Ok(())
137//! # }
138//! ```
139//!
140//! ----
141//!
142//! Asynchronous JS can be called in 2 ways;
143//!
144//! The first is to use the 'async' keyword in JS, and then call the function using [`Runtime::call_function_async`]
145//! ```rust
146//! use rustyscript::{ Runtime, Module, json_args };
147//!
148//! # fn main() -> Result<(), rustyscript::Error> {
149//! let module = Module::new("test.js", "export async function foo() { return 5; }");
150//! let mut runtime = Runtime::new(Default::default())?;
151//!
152//! // The runtime has its own tokio runtime; you can get a handle to it with [Runtime::tokio_runtime]
153//! // You can also build the runtime with your own tokio runtime, see [Runtime::with_tokio_runtime]
154//! let tokio_runtime = runtime.tokio_runtime();
155//!
156//! let result: i32 = tokio_runtime.block_on(async {
157//!     // Top-level await is supported - we can load modules asynchronously
158//!     let handle = runtime.load_module_async(&module).await?;
159//!
160//!     // Call the function asynchronously
161//!     runtime.call_function_async(Some(&handle), "foo", json_args!()).await
162//! })?;
163//!
164//! assert_eq!(result, 5);
165//! # Ok(())
166//! # }
167//! ```
168//!
169//! The second is to use [`crate::js_value::Promise`]
170//! ```rust
171//! use rustyscript::{ Runtime, Module, js_value::Promise, json_args };
172//!
173//! # fn main() -> Result<(), rustyscript::Error> {
174//! let module = Module::new("test.js", "export async function foo() { return 5; }");
175//!
176//! let mut runtime = Runtime::new(Default::default())?;
177//! let handle = runtime.load_module(&module)?;
178//!
179//! // We call the function without waiting for the event loop to run, or for the promise to resolve
180//! // This way we can store it and wait for it later, without blocking the event loop or borrowing the runtime
181//! let result: Promise<i32> = runtime.call_function_immediate(Some(&handle), "foo", json_args!())?;
182//!
183//! // We can then wait for the promise to resolve
184//! // We can do so asynchronously, using [crate::js_value::Promise::into_future]
185//! // But we can also block the current thread:
186//! let result = result.into_value(&mut runtime)?;
187//! assert_eq!(result, 5);
188//! # Ok(())
189//! # }
190//! ```
191//!
192//! - See [`Runtime::register_async_function`] for registering and calling async rust from JS
193//! - See `examples/async_javascript.rs` for a more detailed example of using async JS
194//!
195//! ----
196//!
197//! For better performance calling rust code, consider using an extension instead of a module - see the `runtime_extensions` example for details
198//!
199//! ----
200//!
201//! A threaded worker can be used to run code in a separate thread, or to allow multiple concurrent runtimes.
202//!
203//! the [`worker`] module provides a simple interface to create and interact with workers.
204//! The [`worker::InnerWorker`] trait can be implemented to provide custom worker behavior.
205//!
206//! It also provides a default worker implementation that can be used without any additional setup:
207//! ```ignore
208//! use rustyscript::{Error, worker::{Worker, DefaultWorker, DefaultWorkerOptions}};
209//! use std::time::Duration;
210//!
211//! fn main() -> Result<(), Error> {
212//!     let worker = DefaultWorker::new(DefaultWorkerOptions {
213//!         default_entrypoint: None,
214//!         timeout: Duration::from_secs(5),
215//!     })?;
216//!
217//!     let result: i32 = worker.eval("5 + 5".to_string())?;
218//!     assert_eq!(result, 10);
219//!     Ok(())
220//! }
221//! ```
222//!
223//! ----
224//!
225//! ## Utility Functions
226//! These functions provide simple one-liner access to common features of this crate:
227//! - `evaluate`; Evaluate a single JS expression and return the resulting value
228//! - `import`; Get a handle to a JS module from which you can get exported values and functions
229//! - `resolve_path`; Resolve a relative path to the current working dir
230//! - `validate`; Validate the syntax of a JS expression
231//! - `init_platform`; Initialize the V8 platform for multi-threaded applications
232//!
233//! Commonly used features have been grouped into the following feature-sets:
234//! - **`safe_extensions`** - On by default, these extensions are safe to use in a sandboxed environment
235//! - **`network_extensions`** - These extensions break sandboxing by allowing network connectivity
236//! - **`io_extensions`** - These extensions break sandboxing by allowing filesystem access (WARNING: Also allows some network access)
237//! - **`all_extensions`** - All 3 above groups are included
238//! - **`extra_features`** - Enables the `worker` feature (enabled by default), and the `snapshot_builder` feature
239//! - **`node_experimental`** - HIGHLY EXPERIMENTAL nodeJS support that enables all available Deno extensions
240//!
241//! ## Crate features
242//! The table below lists the available features for this crate. Features marked at `Preserves Sandbox: NO` break isolation between loaded JS modules and the host system.
243//! Use with caution.
244//!
245//! More details on the features can be found in `Cargo.toml`
246//!
247//! Please note that the `web` feature will also enable `fs_import` and `url_import`, allowing arbitrary filesystem and network access for import statements
248//! - This is because the `deno_web` crate allows both fetch and FS reads already
249//!
250//! | Feature           | Description                                                                                               | Preserves Sandbox| Dependencies                                                                                  |  
251//! |-------------------|-----------------------------------------------------------------------------------------------------------|------------------|-----------------------------------------------------------------------------------------------|
252//! |`broadcast_channel`|Implements the web-messaging API for Deno                                                                  |**NO**            |`deno_broadcast_channel`, `deno_web`, `deno_webidl`                                            |
253//! |`cache`            |Implements the Cache API for Deno                                                                          |**NO**            |`deno_cache`, `deno_webidl`, `deno_web`, `deno_crypto`, `deno_fetch`, `deno_url`, `deno_net`   |
254//! |`console`          |Provides `console.*` functionality from JS                                                                 |yes               |`deno_console`, `deno_terminal`                                                                |
255//! |`cron`             |Implements scheduled tasks (crons) API                                                                     |**NO**            |`deno_cron`, `deno_console`                                                                    |
256//! |`crypto`           |Provides `crypto.*` functionality from JS                                                                  |yes               |`deno_crypto`, `deno_webidl`                                                                   |
257//! |`ffi`              |Dynamic library ffi features                                                                               |**NO**            |`deno_ffi`                                                                                     |
258//! |`fs`               |Provides ops for interacting with the file system.                                                         |**NO**            |`deno_fs`, `web`,  `io`                                                                        |
259//! |`http`             |Implements the fetch standard                                                                              |**NO**            |`deno_http`, `web`, `websocket`                                                                |
260//! |`kv`               |Implements the Deno KV Connect protocol                                                                    |**NO**            |`deno_kv`, `web`, `console`                                                                    |
261//! |`url`              |Provides the `URL`, and `URLPattern` APIs from within JS                                                   |yes               |`deno_webidl`, `deno_url`                                                                      |
262//! |`io`               |Provides IO primitives such as stdio streams and abstraction over File System files.                       |**NO**            |`deno_io`, `rustyline`, `winapi`, `nix`, `libc`, `once_cell`                                   |
263//! |`web`              |Provides the `Event`, `TextEncoder`, `TextDecoder`, `File`, Web Cryptography, and fetch APIs from within JS|**NO**            |`deno_webidl`, `deno_web`, `deno_crypto`, `deno_fetch`, `deno_url`, `deno_net`                 |
264//! |`webgpu`           |Implements the WebGPU API                                                                                  |**NO**            |`deno_webgpu`, `web`                                                                           |
265//! |`webstorage`       |Provides the `WebStorage` API                                                                              |**NO**            |`deno_webidl`, `deno_webstorage`                                                               |
266//! |`websocket`        |Provides the `WebSocket` API                                                                               |**NO**            |`deno_web`, `deno_websocket`                                                                   |
267//! |`webidl`           |Provides the `webidl` API                                                                                  |yes               |`deno_webidl`                                                                                  |
268//! |                   |                                                                                                           |                  |                                                                                               |
269//! |`default`          |Provides only those extensions that preserve sandboxing                                                    |yes               |`deno_console`, `deno_crypto`, `deno_webidl`, `deno_url`                                       |
270//! |`no_extensions`    |Disables all extensions to the JS runtime - you can still add your own extensions in this mode             |yes               |None                                                                                           |
271//! |`all`              |Provides all available functionality                                                                       |**NO**            |`deno_console`, `deno_webidl`, `deno_web`, `deno_net`, `deno_crypto`, `deno_fetch`, `deno_url` |
272//! |                   |                                                                                                           |                  |                                                                                               |
273//! |`fs_import`        |Enables importing arbitrary code from the filesystem through JS                                            |**NO**            |None                                                                                           |
274//! |`url_import`       |Enables importing arbitrary code from network locations through JS                                         |**NO**            |`reqwest`                                                                                      |
275//! |                   |                                                                                                           |                  |                                                                                               |
276//! |`node_experimental`|HIGHLY EXPERIMENTAL nodeJS support that enables all available Deno extensions                              |**NO**            |For complete list, see Cargo.toml                                                              |
277//! |                   |                                                                                                           |                  |                                                                                               |
278//! |`worker`           |Enables access to the threaded worker API [`worker`]                                                       |yes               |None                                                                                           |
279//! |`snapshot_builder` |Enables access to [`SnapshotBuilder`], a runtime for creating snapshots that can improve start-times       |yes               |None                                                                                           |
280//! |`web_stub`         |Enables a subset of `web` features that do not break sandboxing                                            |yes               |`deno_webidl`                                                                                  |
281//!
282//! ----
283//!
284//! For an example of this crate in use, see [Lavendeux](https://github.com/rscarson/lavendeux)
285//!
286#![warn(missing_docs)]
287#![warn(clippy::pedantic)]
288#![allow(clippy::module_name_repetitions)] //   Does not account for crate-level re-exports
289#![allow(clippy::inline_always)] //             Does not account for deno_core's use of inline(always) on op2
290#![allow(clippy::needless_pass_by_value)] //    Disabling some features can trigger this
291#![cfg_attr(docsrs, feature(doc_cfg))]
292
293#[cfg(feature = "snapshot_builder")]
294mod snapshot_builder;
295
296#[cfg(feature = "snapshot_builder")]
297#[cfg_attr(docsrs, doc(cfg(feature = "snapshot_builder")))]
298pub use snapshot_builder::SnapshotBuilder;
299
300mod runtime_builder;
301pub use runtime_builder::RuntimeBuilder;
302
303pub mod error;
304pub mod js_value;
305pub mod module_loader;
306pub mod static_runtime;
307
308mod async_bridge;
309mod ext;
310mod inner_runtime;
311mod module;
312mod module_handle;
313mod module_wrapper;
314mod runtime;
315mod traits;
316mod transpiler;
317mod utilities;
318
319#[cfg(feature = "worker")]
320#[cfg_attr(docsrs, doc(cfg(feature = "worker")))]
321pub mod worker;
322
323// Expose a few dependencies that could be useful
324pub use deno_core;
325pub use deno_core::serde_json;
326pub use tokio;
327
328/// Re-exports of the deno extension crates used by this library
329pub mod extensions {
330    #[cfg(feature = "broadcast_channel")]
331    #[cfg_attr(docsrs, doc(cfg(feature = "broadcast_channel")))]
332    pub use deno_broadcast_channel;
333
334    #[cfg(feature = "cache")]
335    #[cfg_attr(docsrs, doc(cfg(feature = "cache")))]
336    pub use deno_cache;
337
338    #[cfg(feature = "console")]
339    #[cfg_attr(docsrs, doc(cfg(feature = "console")))]
340    pub use deno_console;
341
342    #[cfg(feature = "cron")]
343    #[cfg_attr(docsrs, doc(cfg(feature = "cron")))]
344    pub use deno_cron;
345
346    #[cfg(feature = "crypto")]
347    #[cfg_attr(docsrs, doc(cfg(feature = "crypto")))]
348    pub use deno_crypto;
349
350    #[cfg(feature = "ffi")]
351    #[cfg_attr(docsrs, doc(cfg(feature = "ffi")))]
352    pub use deno_ffi;
353
354    #[cfg(feature = "fs")]
355    #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
356    pub use deno_fs;
357
358    #[cfg(feature = "http")]
359    #[cfg_attr(docsrs, doc(cfg(feature = "http")))]
360    pub use deno_http;
361
362    #[cfg(feature = "io")]
363    #[cfg_attr(docsrs, doc(cfg(feature = "io")))]
364    pub use deno_io;
365
366    #[cfg(feature = "kv")]
367    #[cfg_attr(docsrs, doc(cfg(feature = "kv")))]
368    pub use deno_kv;
369
370    #[cfg(feature = "url")]
371    #[cfg_attr(docsrs, doc(cfg(feature = "url")))]
372    pub use deno_url;
373
374    #[cfg(feature = "webgpu")]
375    #[cfg_attr(docsrs, doc(cfg(feature = "webgpu")))]
376    pub use deno_webgpu;
377
378    #[cfg(feature = "websocket")]
379    #[cfg_attr(docsrs, doc(cfg(feature = "websocket")))]
380    pub use deno_websocket;
381
382    #[cfg(feature = "webstorage")]
383    #[cfg_attr(docsrs, doc(cfg(feature = "webstorage")))]
384    pub use deno_webstorage;
385
386    #[cfg(feature = "web")]
387    #[cfg_attr(docsrs, doc(cfg(feature = "webstorage")))]
388    pub use deno_tls;
389}
390
391#[cfg(feature = "kv")]
392#[cfg_attr(docsrs, doc(cfg(feature = "kv")))]
393pub use ext::kv::{KvConfig, KvStore};
394
395#[cfg(feature = "cache")]
396#[cfg_attr(docsrs, doc(cfg(feature = "cache")))]
397pub use ext::cache::CacheBackend;
398
399#[cfg(feature = "node_experimental")]
400#[cfg_attr(docsrs, doc(cfg(feature = "node_experimental")))]
401pub use ext::node::RustyResolver;
402
403#[cfg(feature = "web")]
404#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
405pub use ext::web::{
406    AllowlistWebPermissions, DefaultWebPermissions, PermissionDenied, SystemsPermissionKind,
407    WebOptions, WebPermissions,
408};
409pub use ext::ExtensionOptions;
410
411// Expose some important stuff from us
412pub use error::Error;
413pub use inner_runtime::{RsAsyncFunction, RsFunction};
414pub use module::Module;
415pub use module_handle::ModuleHandle;
416pub use module_wrapper::ModuleWrapper;
417pub use runtime::{Runtime, RuntimeOptions, Undefined};
418pub use utilities::{evaluate, import, init_platform, resolve_path, validate};
419
420#[cfg(feature = "broadcast_channel")]
421#[cfg_attr(docsrs, doc(cfg(feature = "broadcast_channel")))]
422pub use ext::broadcast_channel::BroadcastChannelWrapper;
423
424#[cfg(feature = "web")]
425#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
426pub use hyper_util;
427
428#[cfg(test)]
429mod test {
430    use crate::{include_module, Error, Module, Runtime, RuntimeOptions};
431
432    static WHITELIST: Module = include_module!("op_whitelist.js");
433
434    #[test]
435    fn test_readme_deps() {
436        version_sync::assert_markdown_deps_updated!("readme.md");
437    }
438
439    #[test]
440    fn test_html_root_url() {
441        version_sync::assert_html_root_url_updated!("src/lib.rs");
442    }
443
444    #[test]
445    #[cfg(not(feature = "web"))]
446    fn check_op_whitelist() {
447        let inner = || -> Result<(), Error> {
448            let mut runtime = Runtime::new(RuntimeOptions::default())?;
449            runtime.load_module(&WHITELIST)?;
450            let hnd = runtime.load_module(&Module::new(
451                "test_whitelist.js",
452                "
453                import { whitelist } from './op_whitelist.js';
454                let ops = Deno.core.ops.op_op_names();
455                export const unsafe_ops = ops.filter(op => !whitelist.hasOwnProperty(op));
456            ",
457            ))?;
458
459            let unsafe_ops: Vec<String> = runtime.get_value(Some(&hnd), "unsafe_ops")?;
460
461            if !unsafe_ops.is_empty() {
462                println!("Found unsafe ops: {unsafe_ops:?}.\nOnce confirmed safe, add them to `src/op_whitelist.js`");
463                std::process::exit(1);
464            }
465
466            Ok(())
467        };
468
469        inner().expect("Could not verify op safety");
470    }
471}