ferrosaur
ferrosaur derives statically-typed Rust code — à la wasm-bindgen — for
deno_core::JsRuntime
to interface with your JavaScript code.
If you have:
// lib.js
export const slowFib = (n) =>
n === 0 ? 0 : n === 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
... and you write:
// lib.rs
use ferrosaur::js;
#[js(module("lib.js"))]
struct Math;
#[js(interface)]
impl Math {
#[js(func)]
fn slow_fib(&self, n: serde<usize>) -> serde<usize> {}
}
... then you get:
// let rt: &mut JsRuntime;
let lib = Math::main_module_init(rt).await?;
let fib = lib.slow_fib(42, rt)?;
assert_eq!(fib, 267914296);
tip
This is like the inverse of deno_core::op2
:
#[op2]
gives JavaScript programs easy access to your Rust implementation.- ferrosaur gives your Rust program easy access to JavaScript implementations.
License
This project is released under the Apache 2.0 License and the MIT License.
Examples
console.log
Introductory example using console.log
— start from here!
calculator, fibonacci
Embedding ES modules for fun and portable programs.
ts, ts-blank-space
Workflows integrating NPM dependencies and a bundler.
Output of the ts-blank-space
example. Pretty-printing courtesy of
bat
.
Example: console.log
Source code for this example: examples/console
This example shows the gist of ferrosaur:
- You can derive Rust types to represent JavaScript values.
- You can derive Rust implementations to represent JavaScript interfaces.
- You can compose these types and implementations to express JavaScript APIs of arbitrary shapes and complexities.
To run this example, run:
cargo run --package example-console
Getting started
Everything starts with the js
macro:
use ferrosaur::js;
Getting globalThis
Use #[js(global_this)]
to derive a newtype struct that will
hold a reference to globalThis
:
#[js(global_this)]
struct Global;
// (this doesn't need to be named "Global")
Naming JavaScript values
Use #[js(value)]
to derive a newtype struct that will hold an
arbitrary JavaScript value:
/// the `Deno` namespace
#[js(value)]
struct Deno;
Declaring JavaScript APIs
Now that you have these "value types," use #[js(interface)]
to describe them:
Properties
Use #[js(prop)]
to derive a Rust function that will access a corresponding
JavaScript property:
#[js(interface)]
impl Deno {
#[js(prop)]
fn pid(&self) -> serde<u32> {}
// access the `Deno.pid` property
}
// if we were writing TypeScript, this would be:
interface Deno {
readonly pid: number;
}
Data serialization
Thanks to serde_v8
, Rust types that implement Serialize
/DeserializeOwned
can be passed to/from JavaScript. To indicate that a type T
should be converted
using serde_v8
, write it as serde<T>
, like the serde<u32>
above.
Functions
Use #[js(func)]
to derive a Rust function that will call a corresponding
JavaScript function:
#[js(interface)]
impl Global {
/// <https://docs.deno.com/api/web/~/btoa>
#[js(func)]
fn btoa(&self, to_encode: serde<&str>) -> serde<String> {}
}
Preserving object identities
But what if we want more than just the data? What if we would like to keep JavaScript objects and values around so that we can use them later? Here comes the fun part:
Thanks to the FromV8
/ToV8
traits, any Rust type derived using this crate
can also be passed from/to JavaScript (as can any type that implements those traits).
This is the default conversion mechanism if you don't specify types as serde<T>
.
Getting to console.log
Combining these attributes lets you statically declare JavaScript APIs of
arbitrary shapes. For example, here's how you declare the existence of console.log
:
#[js(interface)]
impl Global {
// there's a `console` on `globalThis` ...
#[js(prop)]
fn console(&self) -> Console {}
}
#[js(value)]
struct Console;
#[js(interface)]
impl Console {
// ... which has a `log` function
#[js(func)]
fn log(&self, message: serde<&str>) {}
// note that we are only allowing a single `&str` message for now
}
// if we were writing TypeScript, this would be:
declare global {
namespace globalThis {
var console: Console;
}
}
interface Console {
log(message: string): void;
}
Running everything
Enough declaring! Let's finally run everything:
#[tokio::main]
async fn main() -> Result<()> {
let rt: &mut JsRuntime = &mut deno()?;
// all APIs derived using this crate require a &mut JsRuntime
// here I'm using a preconfigured runtime, see examples/_runtime for more
let global = Global::new(rt);
let console = global.console(rt)?;
let encoded = global.btoa(r#"{"alg":"HS256"}"#, rt)?;
console.log(&encoded, rt)?;
Ok(())
}
This will run the following equivalent JavaScript:
let console = globalThis.console;
let encoded = globalThis.btoa('{"alg":"HS256"}');
console.log(encoded);
Additional setup code for this example
use anyhow::Result;
use example_runtime::{deno, deno_core, JsRuntime};
tip
This page is generated from the example’s source code.
Example: calculator
Source code for this example: examples/calculator
This example showcases ES module loading and method chaining.
To run this example, run:
cargo run --package example-calculator
Use #[js(module)]
to embed an ECMAScript module
into your Rust program:
use ferrosaur::js;
#[js(module("./main.js", fast))]
struct Main;
main.js
exports a Calculator
class:
#[js(interface)]
impl Main {
#[js(new)]
fn calculator(&self, value: serde<f64>) -> Calculator {}
// export class Calculator ...
}
#[js(value)]
#[derive(Debug)]
struct Calculator;
#[js(interface)]
impl Calculator {
#[js(func)]
fn add(&self, value: serde<f64>) -> Self {}
#[js(func)]
fn sub(&self, value: serde<f64>) -> Self {}
#[js(func)]
fn mul(&self, value: serde<f64>) -> Self {}
#[js(func)]
fn div(&self, value: serde<f64>) -> Self {}
#[js(func(Symbol(toPrimitive)))]
fn print(&self) -> String {}
#[js(prop)]
fn value(&self) -> serde<f64> {}
}
Here's the main function:
#[tokio::main]
async fn main() -> Result<()> {
let rt = &mut JsRuntime::new(RuntimeOptions::default());
// Initialize the module:
let main = Main::main_module_init(rt).await?;
let calc = main
.calculator(1.0, rt)?
.add(2.0, rt)?
.sub(3.0, rt)?
.mul(4.0, rt)?
.div(5.0, rt)?;
// This is https://oeis.org/A261038
println!("RPN: {}", calc.print(rt)?);
// https://en.wikipedia.org/wiki/Reverse_Polish_notation
assert_eq!(calc.value(rt)?, 0.0);
Ok(())
}
Additional setup code for this example
use anyhow::Result;
use deno_core::{JsRuntime, RuntimeOptions};
tip
This page is generated from the example’s source code.
Example: Fibonacci
Source code for this example: examples/fibonacci
The example from Introduction, presented in full.
cargo run --package example-fibonacci
lib.js
/**
* @param {number} n
* @returns {number}
*/
export const slowFib = (n) =>
n === 0 ? 0 : n === 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
main.rs
use ferrosaur::js;
#[js(module("lib.js"))]
struct Math;
#[js(interface)]
impl Math {
#[js(func)]
fn slow_fib(&self, n: serde<usize>) -> serde<usize> {}
}
#[tokio::main]
async fn main() -> Result<()> {
let rt = &mut JsRuntime::try_new(Default::default())?;
let lib = Math::main_module_init(rt).await?;
let fib = lib.slow_fib(42, rt)?;
assert_eq!(fib, 267914296);
Ok(())
}
use anyhow::Result;
use deno_core::JsRuntime;
Example: TypeScript
This example showcases a setup of multiple ES modules that can import from each other,
as well as integration with JavaScript tooling during cargo build
.
tip
For best results, view this page in the book.
It embeds the typescript
compiler in order to type check the example
source code itself.
To run this example, run:
cargo run --package example-ts
important
This example requires the deno
CLI to build.
Output of this example. Errors are expected since typescript
as used in this example
actually does not have access to these types.
tip
See also microsoft/typescript-go, which makes this whole thing kinda silly.
The setup
src/lib.rs
lib.rs
does the following things:
- Embed JS dependencies and export them as Rust structs.
- Provide some reusable interface definitions and utility functions.
The ts-blank-space
example reuses
this module because they also require typescript
.
Embedding typescript
use ferrosaur::js;
#[js(module("../dist/typescript.js", url("npm:typescript"), fast(unsafe_debug)))]
pub struct TypeScriptLib;
-
The embedded file is
"../dist/typescript.js"
. This file is emitted by esbuild duringcargo build
. The actual source file is undersrc/deps
. -
url("npm:typescript")
sets the module specifier to"npm:typescript"
.lib.ts
and other modules in the runtime will then be able to do
import ts from "npm:typescript"
. -
fast(unsafe_debug)
embeds the JS file as a fast V8 string while skipping compile-time assertion that it is in ASCII. This is because thetypescript
lib is massive and doing so will take a long time.esbuild already ensures that its build output is ASCII-only, so it is safe in this case.
Embedding @typescript/vfs
#[js(module(
"../dist/typescript-vfs.js",
url("npm:@typescript/vfs"),
fast(unsafe_debug)
))]
pub struct TypeScriptVfs;
This example additionally uses @typescript/vfs
to setup the files necessary
for typescript
to type check. These files are also embedded into the program,
albeit via a dedicated build step.
@typescript/vfs
is the same system that enables the TypeScript playground
to run in the browser.
Declaring interfaces for lib.ts
#[js(interface)]
pub trait Compiler {
#[js(func)]
fn create_program(&self, root: serde<HashMap<String, String>>) -> Program {}
}
#[js(value)]
pub struct Program;
#[js(interface)]
impl Program {
#[js(func)]
pub fn print_diagnostics(&self, colored: bool) -> serde<String> {}
}
#[js(interface)]
is being used on a trait Compiler
here. This
turns Compiler
into sort of a marker trait,
and enables a form of duck typing.
It is essentially saying "any Rust type that implements Compiler
will provide the
create_program
function." For example, the Example
struct, which embeds lib.ts
:
#[js(module("../dist/lib.js", fast))]
pub struct Example;
impl Compiler for Example {}
Of course, ferrosaur cannot actually verify such an implementation, so it is up to the programmer to guarantee that implementors of such traits actually provide the specified interfaces.
Helpers
inject_lib_dts
This function defines a few properties on globalThis
to be used in the example.
Notably, it injects TYPESCRIPT_LIB
. On the Rust side, this is the embedded
declaration files. On the JavaScript side, this is used to create the
virtual file system.
pub fn inject_lib_dts(rt: &mut JsRuntime) -> Result<()> {
#[js(global_this)]
struct Global;
#[js(interface)]
impl Global {
#[js(set_index)]
fn define_object(&self, name: serde<&str>, value: v8::Global<v8::Object>) {}
#[js(set_index)]
fn define_string(&self, name: serde<&str>, value: serde<&str>) {}
}
let global = Global::new(rt);
let dts = {
let scope = &mut rt.handle_scope();
dts::dts(scope)?
};
global.define_object("TYPESCRIPT_LIB", dts, rt)?;
global.define_string("CARGO_MANIFEST_DIR", env!("CARGO_MANIFEST_DIR"), rt)?;
Ok(())
}
mod dts
lib.dts.rs
is the generated file that embeds declaration files.
See build.rs.
mod dts {
include!(concat!(env!("OUT_DIR"), "/lib.dts.rs"));
}
Additional setup code
use std::collections::HashMap;
use anyhow::Result;
use example_runtime::deno_core::{self, JsRuntime};
src/lib.ts
lib.ts
integrates typescript
and implements functions used in this compiler. It is
embedded by lib.rs
.
File src/lib.ts
import ts from "npm:typescript";
import {
createDefaultMapFromNodeModules,
createSystem,
createVirtualCompilerHost,
} from "npm:@typescript/vfs";
export function createProgram(root: Record<string, string>) {
const options: ts.CompilerOptions = {
strict: true,
noEmit: true,
lib: [ts.getDefaultLibFileName({ target: ts.ScriptTarget.ESNext })],
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.Bundler,
moduleDetection: ts.ModuleDetectionKind.Force,
};
let files: Map<string, string>;
if (globalThis.TYPESCRIPT_LIB) {
files = new Map();
for (const [lib, dts] of Object.entries(globalThis.TYPESCRIPT_LIB)) {
files.set(`/${lib}`, dts);
}
} else {
files = createDefaultMapFromNodeModules(options, ts);
}
for (const [name, src] of Object.entries(root)) {
files.set(name, src);
}
const { compilerHost } = createVirtualCompilerHost(createSystem(files), options, ts);
const program = ts.createProgram(Object.keys(root), options, compilerHost);
return {
printDiagnostics: (colored = true) => {
const diagnostics = [
...program.getGlobalDiagnostics(),
...program.getSyntacticDiagnostics(),
...program.getDeclarationDiagnostics(),
...program.getSemanticDiagnostics(),
];
if (colored) {
return ts.formatDiagnosticsWithColorAndContext(diagnostics, compilerHost);
} else {
return ts.formatDiagnostics(diagnostics, compilerHost);
}
},
};
}
declare global {
namespace globalThis {
var TYPESCRIPT_LIB: Record<string, string> | undefined;
var CARGO_MANIFEST_DIR: string | undefined;
}
}
src/main.rs
#[tokio::main]
async fn main() -> Result<()> {
let rt = &mut deno()?;
Initialize the modules:
use example_ts::{inject_lib_dts, Example, TypeScriptLib, TypeScriptVfs};
TypeScriptLib::side_module_init(rt).await?;
TypeScriptVfs::side_module_init(rt).await?;
let example = Example::main_module_init(rt).await?;
Prepare the VFS. This is very similar to the example in @typescript/vfs
:
inject_lib_dts(rt)?;
let file = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src/lib.ts")
.pipe(std::fs::read_to_string)?;
let root = {
let mut map = HashMap::new();
map.insert("src/lib.ts".into(), file);
map
};
Now, type check:
let program = example.create_program(root, rt)?;
let errors = program.print_diagnostics(true, rt)?;
println!("{errors}");
let errors = program.print_diagnostics(false, rt)?;
insta::assert_snapshot!(errors, @r"
src/lib.ts(1,16): error TS2307: Cannot find module 'npm:typescript' or its corresponding type declarations.
src/lib.ts(6,8): error TS2307: Cannot find module 'npm:@typescript/vfs' or its corresponding type declarations.
");
Ok(())
}
Additional setup code
use std::{collections::HashMap, path::Path};
use anyhow::Result;
use example_runtime::deno;
use example_ts::Compiler;
use tap::Pipe;
build.ts
build.ts
compiles TypeScript files to JavaScript using esbuild at the time of
cargo build
.
Compiling is necessary because:
-
deno_core
itself does not run TypeScript files. -
The
typescript
lib is distributed in CommonJS. esbuild transforms it into ESM so that it can be imported.
File build.ts
import { build, relpath } from "../_runtime/src/lib.ts";
await build({
entryPoints: [relpath("src/lib.ts", import.meta)],
outdir: "dist",
// since we are bundling for ESM, this is required for esbuild to consider
// `typescript`, which is in CJS, to be importable
mainFields: ["module", "main"],
// these modules are made available at runtime
external: ["npm:typescript", "npm:@typescript/vfs"],
bundle: true,
});
await build({
entryPoints: [
relpath("src/deps/typescript.ts", import.meta),
relpath("src/deps/typescript-vfs.ts", import.meta),
],
outdir: "dist",
external: ["fs", "path", "os", "inspector"],
mainFields: ["module", "main"],
bundle: true,
minify: false,
treeShaking: true,
});
build.rs
build.rs
does a few things:
-
Run
build.ts
viadeno
to compile the.ts
files used in this example. -
Generate Rust code that will embed TypeScript's
lib
declarations into the program.These files contain definitions for ECMAScript language APIs, such as essential types like
Promise
, without whichtypescript
will not know how to type check.The generated Rust functions are emitted under
OUT_DIR
and then included inlib.rs
usinginclude!
.
File build.rs
use std::{
path::Path,
process::{Command, Stdio},
};
use anyhow::{bail, Context, Result};
use proc_macro2::TokenStream;
use quote::quote;
fn main() -> Result<()> {
println!("cargo::rerun-if-changed=build.ts");
let built = Command::new("deno")
.args(["run", "--allow-all", "build.ts"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.context("failed to bundle JavaScript resources, is Deno installed?")?
.wait()?
.success();
if !built {
bail!("failed to bundle JavaScript resources")
}
let lib_dts = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../node_modules/typescript/lib")
.read_dir()?
.filter_map(|entry| -> Option<TokenStream> {
let path = entry.ok()?.path();
let name = path.file_name()?.to_str()?;
if !name.ends_with(".d.ts") {
return None;
}
let text = std::fs::read_to_string(&path).ok()?;
let path = path.to_string_lossy();
println!("cargo::rerun-if-changed={path}");
if text.is_ascii() {
Some(quote! {
#[allow(long_running_const_eval)]
(ascii_str!(#name), FastString::from(ascii_str_include!(#path)))
})
} else {
Some(quote! {
(ascii_str!(#name), FastString::from_static(include_str!(#path)))
})
}
});
let lib_dts = quote! {
use ::example_runtime::deno_core::{
anyhow::Result, v8,
FastString, ascii_str, ascii_str_include,
};
pub fn dts(scope: &mut v8::HandleScope) -> Result<v8::Global<v8::Object>> {
let obj = v8::Object::new(scope);
let files = [ #(#lib_dts),* ];
for (lib, dts) in files {
let lib = lib.v8_string(scope)?;
let dts = dts.v8_string(scope)?;
obj.set(scope, lib.into(), dts.into()).unwrap();
}
Ok(v8::Global::new(scope, obj))
}
};
std::fs::write(
Path::new(&std::env::var("OUT_DIR")?).join("lib.dts.rs"),
lib_dts.to_string(),
)?;
Ok(())
}
src/deps/*
These files re-export the respective libs for bundling.
File src/deps/typescript.ts
export { default } from "typescript";
File src/deps/typescript-vfs.ts
export * from "@typescript/vfs";
Example: ts-blank-space
tip
For best results, view this page in the book.
ts-blank-space
is a cool type-stripping TypeScript compiler. This example builds
upon the ts
example to run ts-blank-space
.
tip
"Type-stripping" means erasing TypeScript specific syntax and features from the source code so that it can be directly executed as JavaScript.
To run this example, run:
cargo run --package example-ts-blank-space
important
This example requires the deno
CLI to build.
Output of this example. Notice the extra whitespace in declarations.
Embed ts-blank-space
use ferrosaur::js;
#[js(module("../dist/main.js", fast))]
struct Main;
#[js(interface)]
impl Main {
#[js(func(name = "default"))]
fn blank_space<S: serde::Serialize>(&self, src: serde<S>) -> serde<String> {}
// import { default as blank_space } from "../dist/main.js";
}
The file ../dist/main.js
is emitted by esbuild during cargo build
.
See build.ts
which slightly processes the
ts-blank-space
library so that it can be used in this example.
Setup the runtime
#[tokio::main]
async fn main() -> Result<()> {
let rt = &mut deno()?;
Initialize typescript
use example_ts::{inject_lib_dts, TypeScriptLib, TypeScriptVfs};
TypeScriptLib::side_module_init(rt).await?;
TypeScriptVfs::side_module_init(rt).await?;
TypeScriptLib
and TypeScriptVfs
are provided by the ts
example.
inject_lib_dts(rt)?;
inject_lib_dts
sets up some data that typescript
requires in order to run.
See build.rs
in the ts
example for more info.
Initialize ts-blank-space
let ts = Main::main_module_init(rt).await?;
Run ts-blank-space
on examples/ts/src/lib.ts
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ts/src/lib.ts");
let file = std::fs::read_to_string(&path)?;
let js = ts.blank_space(&file, rt)?;
Evaluate the type-stripped result
#[js(value(of_type(v8::Object)))]
struct Example;
let module: Example = {
let url = Url::from_directory_path(env!("CARGO_MANIFEST_DIR"))
.unwrap()
.join("ad-hoc.js")?;
let id = rt.load_side_es_module_from_code(&url, js.clone()).await?;
rt.mod_evaluate(id).await?;
rt.get_module_namespace(id)?.into()
};
use example_ts::Compiler;
impl Compiler for Example {}
example_ts::Compiler
describes the JavaScript APIs
exported by lib.ts
.
Here we are saying Example
, our ad-hoc ES module produced by ts-blank-space
, comforms
to the interface as described by the Compiler
trait, which is correct.
Pretty-print the type-stripped result
use bat::PrettyPrinter;
PrettyPrinter::new()
.input_from_bytes(js.as_bytes())
.language("javascript")
.theme("GitHub")
.print()?;
println!();
PrettyPrinter
courtesy of bat
.
Use lib.ts
to type check itself
let root = HashMap::new().tap_mut(|map| drop(map.insert("src/lib.ts".into(), file)));
let errors = module
.create_program(root, rt)?
.print_diagnostics(true, rt)?;
println!("{errors}");
{
let mut settings = insta::Settings::clone_current();
settings.set_description("script compiled with ts-blank-space");
settings.set_prepend_module_to_snapshot(false);
settings.set_snapshot_path("../tests/snapshots");
settings.bind(|| insta::assert_snapshot!(js));
}
Ok(())
}
Additional setup code
use std::{collections::HashMap, path::Path};
use anyhow::Result;
use tap::Tap;
use example_runtime::{
deno,
deno_core::{self, serde, url::Url},
};
Reference
Entrypoints
js(module)
, embed and load ES modules.js(global_this)
, access theglobalThis
object.
Newtypes
js(value)
, give arbitrary JavaScript values a Rust type.
Interfaces
js(interface)
, declare object properties, functions, and constructors.js(callable)
, store JavaScript functions as values.js(iterator)
, bridge between JavaScript and Rust iterators.
#[js(global_this)]
Use #[js(global_this)]
for access to the JavaScript globalThis
object:
use ferrosaur::js;
// use it on a unit struct:
#[js(global_this)]
struct Global;
// (struct name does not need to be `Global`)
Call the new
method to initialize it:
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::global::Global;
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;
let global = Global::new(rt);
Ok::<_, anyhow::Error>(())
After this, you can use #[js(interface)]
to further derive access to
properties, functions, and more, on
globalThis
:
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::global::Global;
#[js(interface)]
impl Global {
#[js(func)]
fn atob(&self, to_decode: String) -> String {}
}
Ok::<_, anyhow::Error>(())
The atob
function.
Derived APIs
Methods
pub fn new(rt: &mut JsRuntime) -> Self
Create a handle to the globalThis
object from the given JsRuntime
.
Trait implementations
impl AsRef<v8::Global<v8::Object>> for Global
impl<'a> ToV8<'a> for Global
impl<'a> ToV8<'a> for &'_ Global
#[js(module)]
Use #[js(module)]
to embed an ECMAScript module in the program.
use ferrosaur::js;
#[js(module("../examples/js/mod.js"))]
pub struct Module;
The path is relative to the current file (it has the same usage as the include_str!
macro).
Call the
main_module_init
or
side_module_init
method to initialize it as a main module or side module in the given JsRuntime
.
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::modules::Main as MainModule;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;
let main = MainModule::main_module_init(rt).await?;
Ok(())
}
note
For the difference between a main module and a side module, see documentation for the
corresponding JsRuntime
methods:
After this, you can use #[js(interface)]
to further derive access to
items exported from your module. For example, if you have:
export const answer = "42";
Then you can write:
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::modules::{Main as MainModule};
#[js(interface)]
impl MainModule {
#[js(prop)]
fn answer(&self) -> String {}
}
Ok::<_, anyhow::Error>(())
Sections
Option fast
use ferrosaur::js;
#[js(module("../examples/js/mod.js", fast))]
pub struct Module;
Without the fast
option, JavaScript source code is embedded using include_str!
.
With the fast
option, JavaScript source code is embedded using
deno_core::ascii_str_include!
instead.
The JS source file must be in 7-bit ASCII. It is a compile-time error if this does not hold.
note
For what it means for the string to be "fast," from deno_core::FastStaticString
:
A static string that is compile-time checked to be ASCII and is stored in the most efficient possible way to create V8 strings.
fast(unsafe_debug)
use ferrosaur::js;
#[js(module("../examples/js/mod.js", fast(unsafe_debug)))]
pub struct Module;
Like fast
, except for debug builds, at compile time, unsafely embed
JS code as FastStaticString
s without checking it is in 7-bit ASCII.
For release builds, this behaves the same as fast
. Under the hood, this uses the
#[cfg(debug_assertions)]
condition.
The behavior is undefined if the file is not actually in ASCII.
This could be useful if the source file you are trying to embed is very large, in which case the compile-time checking could take a very long time.
Option url(...)
Control the value of import.meta.url
within the module:
url(preserve)
use ferrosaur::js;
#[js(module("../examples/js/mod.js", url(preserve)))]
pub struct Module;
import.meta.url
will be file:///
followed by the relative path from
CARGO_MANIFEST_DIR
to the embedded JS file. This is the default behavior
if the url(...)
option is not specified.
Example | |
---|---|
JavaScript file | <CARGO_MANIFEST_DIR>/src/js/index.js |
import.meta.url | "file:///src/js/index.js" |
url(cwd)
use ferrosaur::js;
#[js(module("../examples/js/mod.js", url(cwd)))]
pub struct Module;
import.meta.url
will be file://
+ std::env::current_dir()
at runtime + a name
generated from the file's relative path.
This essentially gives import.meta.url
the same real working directory as the program
itself.
Example | |
---|---|
JavaScript file | <CARGO_MANIFEST_DIR>/src/js/index.js |
current_dir() | /path/to/cwd |
import.meta.url | "file:///path/to/cwd/-src-js-index.js" |
url("...")
use ferrosaur::js;
#[js(module("../examples/js/mod.js", url("...")))]
pub struct Module;
Use a custom import.meta.url
.
The string must be parsable by url::Url
. It is a runtime error if the URL is not
parsable. Notably, this means you cannot use a bare identifier like "package"
as you
would with Node.
For example, url("npm:lodash")
sets import.meta.url
to "npm:lodash"
. Other modules
in the same JsRuntime
will then be able to import this module using
import ... from "npm:lodash"
.
Derived APIs
Methods
pub async fn main_module_init(rt: &mut JsRuntime) -> anyhow::Result<Self>
Initialize the embedded ES module as a main
module in the given JsRuntime
.
pub async fn side_module_init(rt: &mut JsRuntime) -> anyhow::Result<Self>
Initialize the embedded ES module as a side
module in the given JsRuntime
.
pub fn module_url() -> anyhow::Result<ModuleSpecifier>
Get the import.meta.url
within the module (controllable through the
url(...)
option).
Associated items
pub const MODULE_SRC: &str or FastStaticString
The embedded JS source code as a constant.
Trait implementations
impl AsRef<v8::Global<v8::Object>> for Module
impl<'a> ToV8<'a> for Module
impl<'a> ToV8<'a> for &'_ Module
#[js(value)]
Use #[js(value)]
to represent arbitrary JavaScript values as types in Rust's type
system.
use ferrosaur::js;
#[js(value)]
struct Lorem;
The derived types are not intended to be instantiated directly. Instead, you can return
them from APIs that you declare on js(global_this)
, a
js(module)
, or another js(value)
. To declare APIs, use
js(interface)
.
Illustrative example: The To-do List
use ferrosaur::js;
#[js(module("../examples/js/mod.js"))]
struct Module;
#[js(interface)]
impl Module {
#[js(prop)]
fn todos(&self) -> TodoList {}
}
#[js(value)]
struct TodoList;
#[js(interface)]
impl TodoList {
#[js(func)]
fn create(&self) -> Todo {}
}
#[js(value)]
struct Todo;
#[js(interface)]
impl Todo {
#[js(prop(with_setter))]
fn done(&self) -> bool {}
}
tip
Types derived with js(value)
, js(module)
, and js(global_this)
are essentially
newtypes around V8 types.
Sections
Option of_type(T)
By default, js(value)
generates a struct that is:
use deno_core::v8;
struct Lorem(v8::Global<v8::Value>);
// ^ inner type
By using the of_type
option, you can use some other V8 data types for the inner type.
For example:
use ferrosaur::js;
#[js(value(of_type(v8::Promise)))]
struct Response;
// struct Response(v8::Global<v8::Promise>);
It should make sense for the data type T
to be placed in a v8::Global
. In
particular, this means v8::Local<v8::Value>
implements TryInto<v8::Local<T>>
.
This could be useful if you want to have simple runtime type checking for your types.
For example, given the Response
type above, if a JS function is supposed to return a
Response
, i.e. a Promise
, but it returns undefined
, then the corresponding Rust
function returns Err(...)
instead of Ok(Response)
.
Note that this is "type checking" only in so far as v8
can try-convert between
different V8 types; this is not TypeScript-style structural typing.
note
See Specifying types for more info on how you can specify types when using this crate.
Derived APIs
In the signatures below,
Type
is the type that you applyjs(value)
to;<T>
is the one of thev8::*
data types. By default, this isv8::Value
, but you can control it using theof_type
option.
Trait implementations
impl AsRef<v8::Global<T>> for Type
impl From<v8::Global<T>> for Type
impl From<Type> for v8::Global<T>
impl<'a> FromV8<'a> for Type
impl<'a> ToV8<'a> for Type
impl<'a> ToV8<'a> for &'_ Type
#[js(interface)]
Use #[js(interface)]
to declare:
You can use js(interface)
on any type derived using this crate, such as a
js(value)
or a js(module)
. You can even use it on traits,
see the ts
example.
use ferrosaur::js;
// First, declare a type:
#[js(value)]
struct CowSay;
// Then, declare its APIs:
#[js(interface)]
impl CowSay {
#[js(prop)]
fn moo(&self) -> String {}
}
Example: The To-do List
Let's say you have the following JavaScript:
export const todos = todoList();
function todoList() {
const items = [];
const create = () => {
const todo = new Todo();
items.push(todo);
return todo;
};
return { create };
}
class Todo {
done = false;
}
Expressed in TypeScript declarations, this is:
export declare const todos: TodoList;
interface TodoList {
create: () => Todo;
}
interface Todo {
done: boolean;
}
You can then express this in Rust as:
use ferrosaur::js;
#[js(module("../examples/js/mod.js"))]
struct Module;
#[js(interface)]
impl Module {
#[js(prop)]
fn todos(&self) -> TodoList {}
}
#[js(value)]
struct TodoList;
#[js(interface)]
impl TodoList {
#[js(func)]
fn create(&self) -> Todo {}
}
#[js(value)]
struct Todo;
#[js(interface)]
impl Todo {
#[js(prop(with_setter))]
fn done(&self) -> bool {}
}
#[js(prop)]
Use #[js(prop)]
for access to JavaScript properties.
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
#[js(prop)]
fn bar(&self) -> serde<f64> {}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;
let foo: Foo = fixture::eval_value("({ bar: 42 })", rt)?;
// let foo: Foo;
assert_eq!(foo.bar(rt)?, 42.0);
Ok(())
}
// Expressed in TypeScript:
interface Foo {
bar: number;
}
declare let foo: Foo;
assert(foo.bar === 42);
The generated function has the signature:
The return type indicates the expected type of the property, which must implement either
FromV8
(the default) or DeserializeOwned
(if written as serde<...>
).
note
See Specifying types for more info on how you can specify types when using this crate.
Implicitly, the property name is the Rust function name
converted to camelCase, but you can override this using the
name
or Symbol
option.
Option name = "..."
Use the specified string as key when accessing the JS property.
You can also write name(propertyKey)
if the key is identifier-like.
use ferrosaur::js;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
#[js(prop(name = "some bar"))]
fn some_bar(&self) -> serde<Option<u32>> {}
}
// Expressed in TypeScript:
interface Foo {
"some bar": number | null;
}
Option Symbol(...)
Use the specified well-known Symbol when accessing the JS property. The Symbol should be in camel case (i.e. the same as in JS).
use ferrosaur::js;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
#[js(prop(Symbol(toStringTag)))]
fn to_string_tag(&self) -> serde<String> {}
}
// Expressed in TypeScript:
interface Foo {
[Symbol.toStringTag]: string;
}
Option with_setter
Generate a setter function in addition to a getter function.
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
#[js(prop(with_setter))]
fn bar(&self) -> serde<f64> {}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;
let foo: Foo = fixture::eval_value("({ bar: 41 })", rt)?;
// let foo: Foo;
assert_eq!(foo.bar(rt)?, 41.0);
foo.set_bar(42.0, rt)?;
assert_eq!(foo.bar(rt)?, 42.0);
Ok(())
}
// Expressed in TypeScript:
interface Foo {
bar: number;
}
declare let foo: Foo;
assert(foo.bar === 41);
foo.bar = 42;
assert(foo.bar === 42);
The generated function has the signature:
where value
has the same type as the getter's declared return type.
#[js(func)]
Use #[js(func)]
for calling JavaScript functions.
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
#[js(value)]
struct Console;
#[js(interface)]
impl Console {
#[js(func)]
fn log(&self, message: serde<&str>) {}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;
let console: Console = fixture::eval_value("({ log: () => {} })", rt)?;
// let console: Console;
console.log("🦀 + 🦕", rt)?;
Ok(())
}
// Expressed in TypeScript:
interface Console {
log(message: string): void;
}
declare let console: Console;
console.log("🦀 + 🦕");
The generated function has the signature:
Argument types must implement either ToV8
(the default) or Serialize
(if written
as serde<T>
). The return type must implement either FromV8
or
DeserializeOwned
.
note
See Specifying types for more info on how you can specify types when using this crate.
Implicitly, the function name is the Rust function name
converted to camelCase, but you can override this using the
name
or Symbol
option.
async
functions
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::global::Global;
#[js(interface)]
impl Global {
#[js(prop(name(Promise)))]
fn promise_constructor(&self) -> PromiseConstructor {}
}
#[js(value)]
struct PromiseConstructor;
#[js(interface)]
impl PromiseConstructor {
#[js(func)]
async fn resolve(&self, value: serde<u64>) -> serde<u64> {}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;
let global = Global::new(rt);
#[allow(non_snake_case)]
let Promise = global.promise_constructor(rt)?;
// let Promise: PromiseConstructor;
assert_eq!(Promise.resolve(42, rt).await?, 42);
Ok(())
}
// Expressed in TypeScript:
interface PromiseConstructor {
resolve(value: number): Promise<number>;
}
declare let Promise: PromiseConstructor;
assert((await Promise.resolve(42)) === 42);
The generated function will be an async fn
. The returned Future
will be ready once
the underlying JS value fulfills.
Internally, this calls JsRuntime::with_event_loop_promise
, which means you don't
need to drive the event loop separately.
this
argument
By default, the JS function will receive the object from which the function is accessed
(i.e. &self
) as its this
value. Expressed in TypeScript, the way your function is
invoked is roughly:
interface Foo {
bar: () => void;
}
declare const foo: Foo;
const bar = foo.bar;
bar.call(foo);
Alternatively, you can explicitly declare the type of this
using the second argument:
this: undefined
use ferrosaur::js;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
#[js(func)]
fn bar(&self, this: undefined) {}
}
// Expressed in TypeScript:
const bar = foo.bar;
bar.call(undefined);
The JS function will receive a this
value of undefined
when called.
The resulting Rust function will not have a this
argument.
this: [SomeType]
use ferrosaur::js;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
#[js(func)]
fn bar(&self, this: Baz) {}
}
#[js(value)]
struct Baz;
// Expressed in TypeScript:
interface Foo {
bar: (this: Baz) => void;
}
const bar = foo.bar;
declare const baz: Baz;
bar.call(baz);
The resulting Rust function will have an explicit this
argument, for which you will
supply a value at call time; the argument will be subject to the same
type conversion rules as other arguments.
Spread arguments
To indicate an argument should be flattened using the spread syntax at call time, prefix
the argument name with ..
(2 dots):
use deno_core::v8;
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::global::Global;
#[js(value)]
struct Console;
#[js(interface)]
impl Console {
#[js(func(name(log)))]
pub fn log(&self, ..values: Vec<String>) {}
// ^
}
// let rt: &mut JsRuntime;
let rt = &mut fixture::deno()?;
// let console: Console;
let console: Console = fixture::eval_value("({ log: () => {} })", rt)?;
console.log(vec!["🦀".into(), "🦕".into()], rt)?;
Ok::<_, anyhow::Error>(())
// Expressed in TypeScript:
interface Console {
log: (...values: string[]) => void;
}
declare const console: Console;
console.log(...["🦀", "🦕"]);
On the Rust side, a spread argument of type A
must implement Iterator<Item = T>
,
where T
must implement either ToV8
(the default) or Serialize
(if written as
serde<T>
). When calling the function, pass the argument using normal syntax.
note
See Specifying types for more info on how you can specify types when using this crate.
tip
The syntax ..args: A
is abusing the range pattern syntax, which is
syntactically valid in function arguments.
Option name = "..."
Use the specified string as key when accessing the function. This has the same usage as
js(prop(name))
.
You can also write name(propertyKey)
if the key is identifier-like.
use ferrosaur::js;
#[js(value)]
struct Date;
#[js(interface)]
impl Date {
#[js(func(name(toISOString)))]
fn to_iso_string(&self) -> serde<String> {}
}
Option Symbol(...)
Use the specified well-known Symbol when accessing the function.
This has the same usage as js(prop(Symbol))
.
use ferrosaur::js;
use deno_core::serde_json;
#[js(value)]
struct Date;
#[js(interface)]
impl Date {
#[js(func(Symbol(toPrimitive)))]
fn to_primitive(&self, hint: serde<&str>) -> serde<serde_json::Value> {}
}
#[js(new)]
Use #[js(func)]
for invoking JavaScript constructors.
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
#[js(global_this)]
struct Global;
#[js(interface)]
impl Global {
#[js(new)]
fn date(&self, timestamp: serde<f64>) -> Date {}
}
#[js(value)]
struct Date;
#[js(interface)]
impl Date {
#[js(func(name(toISOString)))]
fn to_iso_string(&self) -> serde<String> {}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;
let global = Global::new(rt);
// let global: Global;
let date = global.date(0.0, rt)?;
assert_eq!(date.to_iso_string(rt)?, "1970-01-01T00:00:00.000Z");
// struct Date;
// impl Date { ... }
Ok(())
}
// Expressed in TypeScript:
let date = new Date(0);
assert(date.toISOString() === "1970-01-01T00:00:00.000Z");
The generated function has the signature:
js(new)
accepts the same function signature format as js(func)
, except
constructors cannot be async
.
Implicitly, the class name is the name of the return type (with case preserved). If the
return type name cannot be used, such as if it is not a simple identifier, or if you
would like to override it, you can use the class
option.
Sections
Note on return type
Note that the return type of the method is the JavaScript type that will be
constructed, whereas Self
represents the JavaScript object from which the
constructor is accessed (such as a module or globalThis
).
In other words, the following usage is almost never what you want:
use ferrosaur::js;
// 🔴 these are almost never what you want
#[js(interface)]
impl Rectangle {
#[js(new)]
fn new() -> Self {}
// or
#[js(new)]
fn new(&self) -> Self {}
}
Instead, you likely want to write:
use ferrosaur::js;
#[js(value)]
struct Shapes;
#[js(interface)]
impl Shapes {
#[js(new)]
fn rectangle(&self) -> Rectangle {}
}
#[js(value)]
struct Rectangle;
// Expressed in TypeScript:
declare const shapes: Shapes;
const Rectangle = shapes.Rectangle;
new Rectangle();
Option class(...)
Use the specified string as key when accessing the constructor, instead of using the
name of the return type. This has the same usage as
js(prop(name))
.
use ferrosaur::js;
#[js(value)]
struct Window;
#[js(interface)]
impl Window {
#[js(new(class(XMLHttpRequest)))]
fn xml_http_request(&self) -> XmlHttpRequest {}
}
#[js(value)]
struct XmlHttpRequest;
#[js(get_index)]
, #[js(set_index)]
Use #[js(get_index)]
and #[js(set_index)]
for dynamic property access and update
(i.e. the obj[prop]
bracket notation).
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
#[js(value)]
struct Record;
#[js(interface)]
impl Record {
#[js(get_index)]
fn get(&self, k: serde<&str>) -> String {}
#[js(set_index)]
fn set(&self, k: serde<&str>, v: serde<&str>) {}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;
let record: Record = fixture::eval_value("({})", rt)?;
// let record: Record;
let key = "foo";
record.set(key, "bar", rt)?;
assert_eq!(record.get(key, rt)?, "bar");
Ok(())
}
// Expressed in TypeScript:
declare const record: Record<string, string>;
const key = "foo";
record[key] = "bar";
assert(record[key] === "bar");
Functions decorated with js(get_index)
must have 2 arguments: &self
and the key to
get, as well as a return type: the type of the value.
Functions decorated with js(set_index)
must have 3 arguments: &self
, the key, and
the value to set.
Argument types must implement either ToV8
(the default) or Serialize
(if written
as serde<T>
). The return type must implement either FromV8
or
DeserializeOwned
.
note
See Specifying types for more info on how you can specify types when using this crate.
#[js(callable)]
Use #[js(callable)]
to store JavaScript functions as values while declaring their
signatures.
This could be used to describe functions that return other functions:
use ferrosaur::js;
#[js(value)]
struct Logger;
#[js(callable)]
impl Logger {
fn call(&self, message: serde<&str>) {}
}
#[js(value)]
struct Logging;
#[js(interface)]
impl Logging {
#[js(func)]
fn with_prefix(&self, prefix: serde<&str>) -> Logger {}
}
// Expressed in TypeScript:
interface Logger {
(message: string): void;
}
interface Logging {
with_prefix: (prefix: string) => Logging;
// (prefix: string) => (message: string) => void
}
Use js(callable)
on an impl
block, which must contain a single item, a function
named call
, whose signature follows the same usage as js(func)
.
tip
JavaScript does not have a proper "callable interface." This is named callable
to
distinguish it from js(func)
, which is for describing named
functions accessible from an object.
#[js(iterator)]
Use #[js(iterator)]
to represent and interact with objects conforming to the iterator
protocol.
use ferrosaur::js;
#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
#[js(value)]
struct MapEntries;
#[js(iterator)]
impl MapEntries {
type Item = serde<(String, String)>;
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;
let mut entries: MapEntries = rt
.execute_script("eval", "new Map([['foo', 'bar']]).entries()")?
.into();
let Ok(Some((k, v))) = entries.next(rt) else { panic!() };
assert_eq!(k, "foo");
assert_eq!(v, "bar");
Ok(())
}
// Expressed in TypeScript:
const entries = new Map([["foo", "bar"]]).entries();
const [k, v] = entries.next().value!;
assert(k === "foo");
assert(v === "bar");
Use js(callable)
on an impl
block. The impl
must contain a single item,
type Item = T
, where T
must implement either FromV8
(the default) or
DeserializeOwned
(if written as serde<T>
).
note
See Specifying types for more info on how you can specify types when using this crate.
Derived APIs
note
Note that js(iterator)
types do not implement the Iterator
trait: both of the
below provided APIs require passing in a JsRuntime
at call time.
Methods
pub fn next(&mut self, &mut JsRuntime) -> anyhow::Result<Option<T>>
Receive the next value from the iterator:
Ok(Some(T))
if the next value is successfully returned;Ok(None)
if the iterator has been exhausted;Err(...)
if there was an error while advancing the iterator.
More specifically, the returned value depends on the value returned by the underlying
next()
function:
JavaScript value | Rust value |
---|---|
{ done?: false, value: T } | Ok(Some(T)) |
{ done: true, value: T } | Ok(Some(T)) |
{ done: true, value?: undefined } | Ok(None) |
Exception caught | Err(...) |
pub fn into_iter<'a>(self, rt: &'a mut JsRuntime)
-> impl Iterator<Item = anyhow::Result<T>> + use<'a>
Get a proper Rust Iterator
, which produces anyhow::Result<T>
.
This enables you to use all the capabilities of a Rust iterator, such as
collect()
, as well as using it in a for
loop.
note
Due to lifetime restrictions, the returned iterator mutably borrows the JsRuntime
for the entire duration of the iteration. This will prevent you from using it on the
produced items until the iterator is dropped.
To be able to use the runtime during iteration, manually call
next(&mut JsRuntime)
.
Specifying types
To be able to interact with JavaScript, code generated by this crate must convert between Rust types and V8 data types. Function arguments and return types must implement specific conversion traits.
ToV8
and FromV8
ToV8
and FromV8
are deno_core
's builtin conversion traits.
By default:
-
Function arguments must implement
ToV8
;- In the case of variadic functions, the
argument must implement
Iterator<Item = T>
, andT
must implementToV8
;
- In the case of variadic functions, the
argument must implement
-
Function return types, property accessor return types, and iterator item types must implement
FromV8
.
In addition to existing implementors,
-
Types derived with
js(value)
,js(module)
, andjs(global_this)
implementToV8
. This means you can pass such values to JS functions as arguments. -
Types derived with
js(value)
implementFromV8
. This means you can return such values from JS functions.
Serialize
and DeserializeOwned
Alternatively, you can opt in to data conversion using serde_v8
. To do so, wrap the
type in serde<...>
:
use ferrosaur::js;
#[js(value)]
struct Foo;
use deno_core::serde::{Serialize, de::DeserializeOwned};
#[js(interface)]
impl Foo {
#[js(func)]
fn bar<T, U>(&self, baz: serde<T>) -> serde<U>
where
T: Serialize,
U: DeserializeOwned,
{}
}
In this case:
-
Function arguments must implement
Serialize
;- In the case of variadic functions, the
argument must implement
Iterator<Item = T>
, andT
must implementSerialize
;
- In the case of variadic functions, the
argument must implement
-
Function return types, property accessor return types, and iterator item types must implement
DeserializeOwned
.
Common pitfalls
the trait bound ToV8<'_>
/FromV8<'_>
is not satisfied
Function argument and return types for js(func)
,
js(prop)
, etc., must implement specific traits for data to be
able to pass to/from JavaScript.
By default, arguments must implement ToV8
, and return types must implement
FromV8
.
If you would like to serialize data using serde
instead, you can opt into this
behavior by rewriting a type T
as serde<T>
:
#[js(interface)]
impl Foo {
#[js(func)]
- fn bar(&self, baz: Baz) {}
+ fn bar(&self, baz: serde<Baz>) {}
}
See Specifying types for additional information on data conversion.
use of undeclared crate or module deno_core
The #[js]
macro generates code that references the deno_core
crate, but it does
not generate a use deno_core
statement. Instead, it assume that deno_core
is in
scope where the macro is used.
If your crate directly depends on deno_core
, then the macro will work without extra
steps. If your crate does not directly depend on deno_core
, for example, if you are
using deno_runtime
instead, then you can manually introduce deno_core
into scope:
+ use deno_runtime::deno_core;
#[js(global_this)]
struct Global;
CHANGELOG
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.1.1
Other changes
Documentation
0.1.0 - 2025-05-07
Initial release!
note
This page is included only for link validation during build.
ferrosaur
So you use deno_core
, and you want to call JavaScript from Rust.
// If you have: lib.js
export const slowFib = (n) =>
n === 0 ? 0 : n === 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
// and you write: lib.rs
use ferrosaur::js;
#[js(module("lib.js"))]
struct Math;
#[js(interface)]
impl Math {
#[js(func)]
fn slow_fib(&self, n: serde<usize>) -> serde<usize> {}
}
// Then you get:
// let rt: &mut JsRuntime;
let lib = Math::main_module_init(rt).await?;
let fib = lib.slow_fib(42, rt)?;
assert_eq!(fib, 267914296);
ferrosaur derives types and implementations, à la wasm-bindgen, that you can use
with your favorite JsRuntime
.
Documentation
You may be looking for:
- Examples
- Reference
js(global_this)
|js(module)
|js(value)
|js(interface)
|js(prop)
|js(func)
|js(new)
License
This project is released under the Apache 2.0 License and the MIT License.
note
This page is included only for link validation during build.
Example: calculator
See documentation at https://tonywu6.github.io/ferrosaur/examples/calculator.
note
This page is included only for link validation during build.
Example: console.log
See documentation at https://tonywu6.github.io/ferrosaur/examples/console.
note
This page is included only for link validation during build.
Example: Fibonacci
This is the example from crate README, presented in full.
See documentation at https://tonywu6.github.io/ferrosaur/examples/fibonacci.