From 01f46f4d1943e77ab22d1a91c50d6f31addd69e1 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Wed, 6 Sep 2023 16:52:22 -0400 Subject: [PATCH 1/3] Start of contributing architecture doc --- docs/contributing-architecture.md | 68 +++++++++++++++++++++++++++++++ docs/contributing.md | 8 ++++ 2 files changed, 76 insertions(+) create mode 100644 docs/contributing-architecture.md diff --git a/docs/contributing-architecture.md b/docs/contributing-architecture.md new file mode 100644 index 00000000..ad43ec4d --- /dev/null +++ b/docs/contributing-architecture.md @@ -0,0 +1,68 @@ +# Architecture of Javy + +This document is intended to provide an overview of the crates and NPM packages in Javy. + +## Crates + +```mermaid +flowchart TD + javy-cli --> wasm + subgraph wasm[javy_core.wasm / javy_quickjs_provider.wasm] + javy-core --> javy + javy-core --> javy-apis + javy-apis --> javy + javy --> quickjs-wasm-rs + quickjs-wasm-rs --> quickjs-wasm-sys + end +``` + +We anticipate most changes will be to the `javy`, `javy-apis`, and `quickjs-wasm-rs` crates. + +### `javy` + +The entrypoint for working with Javy as a library for third parties. This crate is intended to compile to `wasm32-wasi` and provide ergonomic APIs for configuring a QuickJS-based runtime. If there is a configuration option for QuickJS that would be helpful, this is the place to add it. + +#### Important concepts + +- `javy::Runtime` - a configurable QuickJS runtime. +- `javy::Config` - a configuration for the runtime. + +### `javy-apis` + +Common JS APIs for use with a `javy::Runtime`. For example, `console`, `TextEncoder`, `TextDecoder`. If there is a standard JS API that seems like it would be useful to multiple users of Javy, it should be implemented in this crate. If this is an API specific to your use case, you should define it in a crate of your own and register the implementation using a similar approach to how the APIs in this crate define their implementations. + +#### Adding an API implementation + +1. Add a feature to the crate's `Cargo.toml` for your module. +2. Create a directory under `src` with a `mod.rs` file. +3. If your API implementation requires configuration, create a configuration struct for the configuration properties required in your module. +4. In `mod.rs`, implement the `crate::JSApiSet` trait. If your API requires configuration, add the configuration struct defined earlier to the `crate::ApiConfig` struct under a `#[cfg(feature = "your feature name")]` attribute. +5. If necessary, add any JS source files inside the module you're adding. +6. Add the `mod` to the crate's `lib.rs` under a `#[cfg(feature = "your feature name")]` attribute. +7. Add a call to your struct's `register` method under a `#[cfg(feature = "your feature name")]` in `lib.rs`'s `add_to_runtime` function. + +### `javy-cli` + +The CLI for compiling JS to Wasm. This isn't intended to be a CLI that accommodates all uses for all users but rather to provide a useful base of functionality. This is kind of similar to how Wasmtime ships with a crate and a CLI and doing non-generic things with Wasmtime requires writing your own CLI around the Wasmtime crate. + +### `javy-core` + +Gets compiled to `javy_core.wasm` and `javy_quickjs_provider.wasm` for use by the CLI and in environments for running dynamically linked modules. This isn't intended to be used as a code library by third parties. Contains logic for driving the `javy` crate for Wasm modules generated by `javy-cli`. + +### `quickjs-wasm-rs` + +Provides an ergonomic API around the `quickjs-wasm-sys` crate as well as a `serde` implementations for `JSValueRef`. + +### `quickjs-wasm-sys` + +A Rust wrapper around the QuickJS C library. + +## NPM packages + +### `javy` + +A JS library providing ergonomic helpers around the lower level APIs for I/O exposed by `javy-apis`. + +### `javy-cli` + +The package that enables using the `javy-cli` through NPM. You can use `npx javy-cli` to run various Javy commands. diff --git a/docs/contributing.md b/docs/contributing.md index c0db1da1..90265ec2 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,5 +1,9 @@ # Contributing +## Architecture + +See our [architecture document](contributing-architecture.md) for more information about how the project is organized. + ## Adding additional JS APIs We will only add JS APIs or accept contributions that add JS APIs that are potentially useful across multiple environments and do not invoke non-[WASI](https://wasi.dev/) hostcalls. If you wish to add or use JS APIs that do not meet these criteria, please use the `quickjs-wasm-rs` crate directly. We may revisit how we support importing and exporting custom functionality from Javy once [the Component Model](https://github.com/WebAssembly/component-model) has stabilized. @@ -15,3 +19,7 @@ The versions for `javy` and `javy-apis` must always be the same. So a version ch After publishing a release, immediately update the version number to the next patch version with an `-alpha.1` suffix. The first time an additive change is made, reset the patch version to `0` and increment the minor version and reset the suffix to `-alpha.1`. When making additional additive changes, increment the number in the suffix, for example `-alpha.2`. The first time a breaking change is made, reset the patch version and minor version to `0` and increment the major version and reset the suffix to `-alpha.1`. When making additional breaking changes, increment the number in the suffix, for example `-alpha.2`. When releasing, remove the suffix and then publish. + +## cargo vet + +We use [cargo vet](https://mozilla.github.io/cargo-vet/) to audit dependencies for the project. If you need to change or add dependencies, please try to use a dependency that has been audited by one one of the audits we import or is published by one of the authors we trust (sunfishcode, dtolnay, Amanieu, cuviper). This is preferable to adding new exemptions for the project. Do not add new audits for crates that are not in this project. From 3fd5c0f35e9facc7c4b5be52b390ad7a3df27c8a Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 12 Sep 2023 16:35:36 -0400 Subject: [PATCH 2/3] Add example of adding API to javy-apis --- docs/contributing-architecture.md | 179 +++++++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 2 deletions(-) diff --git a/docs/contributing-architecture.md b/docs/contributing-architecture.md index ad43ec4d..edcf7e0b 100644 --- a/docs/contributing-architecture.md +++ b/docs/contributing-architecture.md @@ -36,11 +36,186 @@ Common JS APIs for use with a `javy::Runtime`. For example, `console`, `TextEnco 1. Add a feature to the crate's `Cargo.toml` for your module. 2. Create a directory under `src` with a `mod.rs` file. 3. If your API implementation requires configuration, create a configuration struct for the configuration properties required in your module. -4. In `mod.rs`, implement the `crate::JSApiSet` trait. If your API requires configuration, add the configuration struct defined earlier to the `crate::ApiConfig` struct under a `#[cfg(feature = "your feature name")]` attribute. -5. If necessary, add any JS source files inside the module you're adding. +4. If necessary, add any JS source files inside the module you're adding. If you can implement your API without JS, you don't need to add any. +5. In `mod.rs`, implement the `crate::JSApiSet` trait. If your API requires configuration, add the configuration struct defined earlier to the `crate::ApiConfig` struct under a `#[cfg(feature = "your feature name")]` attribute. 6. Add the `mod` to the crate's `lib.rs` under a `#[cfg(feature = "your feature name")]` attribute. 7. Add a call to your struct's `register` method under a `#[cfg(feature = "your feature name")]` in `lib.rs`'s `add_to_runtime` function. +##### Example + +Here's a contrived example of adding an API to print an environment variable with a prefix that's configured when creating a Javy runtime. Normally this wouldn't go in this crate, but instead in your own crate since it's not a generally useful API. + +Create the directory, `crates/apis/src/env_var_printer`. + +In `crates/apis/Cargo.toml`: + +```diff + [features] + console = [] ++ env_var_printer = [] + random = ["dep:fastrand"] + stream_io = [] + text_encoding = [] +``` + +In `crates/apis/src/env_var_printer/config.rs`: + +```rust +use crate::APIConfig; + +// Use crate visibility to avoid exposing the property outside the crate +#[derive(Debug)] +pub(crate) struct EnvVarConfig { + pub(super) prefix: String, +} + +// Always have a default value for every config. +impl Default for EnvVarConfig { + fn default() -> Self { + Self { + prefix: "Default prefix: ".to_string(), + } + } +} + +// Define one or more methods on `APIConfig`, not `EnvVarConfig`, to set properties. +impl APIConfig { + /// Sets the prefix for `Javy.Env.print`. + pub fn prefix(&mut self, prefix: String) -> &mut Self { + self.env_var.prefix = prefix; + self + } +} +``` + +In `crates/apis/src/env_var_printer/env-var-printer.js`: + +```js +// Wrap everything in an anonymous function to avoid leaking local variables into the global scope. +(function () { + // Get a reference to the function before we delete it from `globalThis`. + const __javy_env_printEnvVar = globalThis.__javy_env_printVal; + globalThis.Javy.Env = { + print(name) { + __javy_env_printEnvVar(name); + }, + }; + // Delete the function from `globalThis` so it doesn't leak. + Reflect.deleteProperty(globalThis, "__javy_env_printVal"); +})(); +``` + +For something this simple, you don't need a JS file, I'm including it to demonstrate how things would be wired up. + +In `crates/apis/src/env_var_printer/mod.rs`: + +```rust +use std::env; + +use anyhow::{bail, Result}; +use javy::{quickjs::JSValue, Runtime}; + +use crate::{APIConfig, JSApiSet}; +pub(super) use config::EnvVarConfig; + +mod config; + +pub(super) struct EnvVarPrinter; + +impl JSApiSet for EnvVarPrinter { + fn register(&self, runtime: &Runtime, config: &APIConfig) -> Result<()> { + let context = runtime.context(); + + let global = context.global_object()?; + + let mut javy_object = global.get_property("Javy")?; + + // If you're defining something on the `Javy` object, ensure it exists. + if javy_object.is_undefined() { + javy_object = context.object_value()?; + global.set_property("Javy", javy_object)?; + } + + // `wrap_callback`` has a static lifetime so you can't use references to the config in its body. + let prefix = config.env_var.prefix.clone(); + global.set_property( + "__javy_env_printVal", + context.wrap_callback(move |_ctx, _this, args| { + let [name] = args else { + bail!("Incorrect number of arguments"); + }; + // Convert JSValueRefs to Rust types. + let name: String = name.try_into()?; + println!("{}{}", prefix, env::var(name)?); + Ok(JSValue::Undefined) + })?, + )?; + + context.eval_global("env-var-printer.js", include_str!("env-var-printer.js"))?; + + Ok(()) + } +} + +// Automated tests are highly recommended +#[cfg(test)] +mod tests { + use std::env; + + use crate::{APIConfig, JSApiSet}; + use anyhow::Result; + use javy::Runtime; + + use super::EnvVarPrinter; + + #[test] + fn test_print_env_var() -> Result<()> { + let runtime = Runtime::default(); + let context = runtime.context(); + EnvVarPrinter.register(&runtime, &APIConfig::default())?; + env::set_var("HELLO", "there"); + let _ = context.eval_global("main", "Javy.Env.print('HELLO');")?; + env::remove_var("HELLO"); + Ok(()) + } +} +``` + +In `crates/apis/src/api_config.rs`: + +```diff + #[derive(Debug, Default)] + pub struct APIConfig { + #[cfg(feature = "console")] + pub(crate) console: crate::console::ConsoleConfig, ++ #[cfg(feature = "env_var_printer")] ++ pub(crate) env_var: crate::env_var_printer::EnvVarConfig, + } +``` + +In `crates/apis/src/lib.rs`: + +```diff + #[cfg(feature = "console")] + mod console; ++ #[cfg(feature = "env_var_printer")] ++ mod env_var_printer; + #[cfg(feature = "random")] + mod random; +``` + +and + +```diff + pub fn add_to_runtime(runtime: &Runtime, config: APIConfig) -> Result<()> { + #[cfg(feature = "console")] + console::Console::new().register(runtime, &config)?; ++ #[cfg(feature = "env_var_printer")] ++ env_var_printer::EnvVarPrinter.register(runtime, &config)?; + #[cfg(feature = "random")] + random::Random.register(runtime, &config)?; +``` + ### `javy-cli` The CLI for compiling JS to Wasm. This isn't intended to be a CLI that accommodates all uses for all users but rather to provide a useful base of functionality. This is kind of similar to how Wasmtime ships with a crate and a CLI and doing non-generic things with Wasmtime requires writing your own CLI around the Wasmtime crate. From 9bad38b260644a9ff44b63ca136385d8f4b593a5 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 12 Sep 2023 17:23:46 -0400 Subject: [PATCH 3/3] Add example of change to javy crate --- docs/contributing-architecture.md | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/contributing-architecture.md b/docs/contributing-architecture.md index edcf7e0b..053c03f4 100644 --- a/docs/contributing-architecture.md +++ b/docs/contributing-architecture.md @@ -27,6 +27,57 @@ The entrypoint for working with Javy as a library for third parties. This crate - `javy::Runtime` - a configurable QuickJS runtime. - `javy::Config` - a configuration for the runtime. +#### Example of a change + +This is a contrived example of how to make a change. If I want to add a configuuration to set a global variable called `javy_rocks` to `true`, I would do the following: + +In `crates/javy/src/config.rs`: + +```diff + /// A configuration for [`Runtime`](crate::Runtime). + #[derive(Debug)] + pub struct Config { ++ pub(crate) set_javy_rocks: bool, + } + + impl Default for Config { + /// Creates a [`Config`] with default values. + fn default() -> Self { + Self { ++ set_javy_rocks: false, + } + } + } + + impl Config { ++ /// Sets `globalThis.javy_rocks` to `true`. ++ pub fn set_javy_rocks(&mut self) -> &mut Self { ++ self.set_javy_rocks = true; ++ self ++ } + } +``` + +We require creating a method to set the property and it should return `&mut Self` so it can be chained. + +In `crates/javy/src/runtime.rs`: + +```diff + impl Runtime { + /// Creates a new [`Runtime`]. + pub fn new(config: Config) -> Result { + let context = JSContextRef::default(); ++ if config.set_javy_rocks { ++ context ++ .global_object()? ++ .set_property("javy_rocks", context.value_from_bool(true)?)?; ++ } + Ok(Self { context }) + } +``` + +Read the `config` and call the appropriate methods on `context` to apply the configuration. + ### `javy-apis` Common JS APIs for use with a `javy::Runtime`. For example, `console`, `TextEncoder`, `TextDecoder`. If there is a standard JS API that seems like it would be useful to multiple users of Javy, it should be implemented in this crate. If this is an API specific to your use case, you should define it in a crate of your own and register the implementation using a similar approach to how the APIs in this crate define their implementations.