Skip to content

Commit

Permalink
feat: support semver aware WIT
Browse files Browse the repository at this point in the history
Signed-off-by: Victor Adossi <vadossi@cosmonic.com>
  • Loading branch information
vados-cosmonic committed Aug 1, 2024
1 parent 3946278 commit 65a8f7c
Show file tree
Hide file tree
Showing 16 changed files with 512 additions and 58 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ strip = true
anyhow = "1.0.86"
base64 = "0.22.1"
heck = "0.5.0"
log = "0.4.22"
semver = "1.0.23"
js-component-bindgen = { path = "./crates/js-component-bindgen" }
structopt = "0.3.26"
wasm-encoder = "0.212.0"
Expand All @@ -55,4 +57,4 @@ wit-parser = "0.212.0"
xshell = "0.2.6"

[dev-dependencies]
anyhow = { workspace = true }
anyhow = { workspace = true }
14 changes: 14 additions & 0 deletions crates/js-component-bindgen-component/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ impl Guest for JsComponentBindgenComponent {
opts: TypeGenerationOptions,
) -> Result<Vec<(String, Vec<u8>)>, String> {
let mut resolve = Resolve::default();

// Add features if specified
match opts.features {
Some(EnabledFeatureSet::List(ref features)) => {
for f in features.into_iter() {
resolve.features.insert(f.to_string());
}
}
Some(EnabledFeatureSet::All) => {
resolve.all_features = true;
}
_ => {}
}

let ids = match opts.wit {
Wit::Source(source) => resolve
.push_str(format!("{name}.wit"), &source)
Expand Down
10 changes: 10 additions & 0 deletions crates/js-component-bindgen-component/wit/js-component-bindgen.wit
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ world js-component-bindgen {
path(string),
}

/// Enumerate enabled features
variant enabled-feature-set {
/// Enable only the given list of features
%list(list<string>),
/// Enable all features
all,
}

record type-generation-options {
/// wit to generate typing from
wit: wit,
Expand All @@ -81,6 +89,8 @@ world js-component-bindgen {
tla-compat: option<bool>,
instantiation: option<instantiation-mode>,
map: option<maps>,
/// Features that should be enabled as part of feature gating
features: option<enabled-feature-set>,
}

enum export-type {
Expand Down
6 changes: 4 additions & 2 deletions crates/js-component-bindgen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ transpile-bindgen = []

[dependencies]
anyhow = { workspace = true }
base64 = { workspace = true }
heck = { workspace = true }
log = { workspace = true }
semver = { workspace = true }
wasm-encoder = { workspace = true }
wasmparser = { workspace = true }
wasmtime-environ = { workspace = true, features = ['component-model'] }
wit-bindgen-core = { workspace = true }
wit-component = { workspace = true }
wit-parser = { workspace = true }
base64 = { workspace = true }
wasm-encoder = { workspace = true }
36 changes: 34 additions & 2 deletions crates/js-component-bindgen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ pub use transpile_bindgen::{BindingsMode, InstantiationMode, TranspileOpts};
use anyhow::Result;
use transpile_bindgen::transpile_bindgen;

use anyhow::{bail, Context};
use anyhow::{bail, ensure, Context};
use wasmtime_environ::component::{ComponentTypesBuilder, Export, StaticModuleIndex};
use wasmtime_environ::wasmparser::Validator;
use wasmtime_environ::{PrimaryMap, ScopeVec, Tunables};
use wit_component::DecodedWasm;

use ts_bindgen::ts_bindgen;
use wit_parser::{Resolve, Type, TypeDefKind, TypeId, WorldId};
use wit_parser::{Package, Resolve, Stability, Type, TypeDefKind, TypeId, WorldId};

/// Calls [`write!`] with the passed arguments and unwraps the result.
///
Expand Down Expand Up @@ -172,3 +172,35 @@ pub fn dealias(resolve: &Resolve, mut id: TypeId) -> TypeId {
}
}
}

/// Check if an item (usually some form of [`WorldItem`]) should be allowed through the feature gate
/// of a given package.
fn feature_gate_allowed(
resolve: &Resolve,
package: &Package,
stability: &Stability,
) -> Result<bool> {
Ok(match stability {
Stability::Unknown => true,
Stability::Stable { since, .. } => {
let Some(package_version) = package.name.version.as_ref() else {
// If the package version is missing (we're likely dealing with an unresolved package)
// and we can't really check much.
return Ok(true);
};

ensure!(
package_version >= since,
"feature gates cannot refer to unreleased (future) package versions"
);

// Stabilization (@since annotation) overrides features and deprecation
true
}
Stability::Unstable { feature } => {
// If a @unstable feature is present but the related feature was not enabled
// or all features was not selected, exclude
resolve.all_features || resolve.features.contains(feature)
}
})
}
115 changes: 91 additions & 24 deletions crates/js-component-bindgen/src/ts_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ use crate::function_bindgen::{array_ty, as_nullable, maybe_null};
use crate::names::{is_js_identifier, maybe_quote_id, LocalNames, RESERVED_KEYWORDS};
use crate::source::Source;
use crate::transpile_bindgen::{parse_world_key, InstantiationMode, TranspileOpts};
use crate::{dealias, uwrite, uwriteln};
use crate::{dealias, feature_gate_allowed, uwrite, uwriteln};
use heck::*;
use log::debug;
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, HashSet};
use std::fmt::Write;
use wit_parser::*;
use wit_bindgen_core::wit_parser::{
Docs, Enum, Flags, Function, FunctionKind, Handle, InterfaceId, Record, Resolve, Result_,
Tuple, Type, TypeDefKind, TypeId, TypeOwner, Variant, WorldId, WorldItem, WorldKey,
};

struct TsBindgen {
/// The source code for the "main" file that's going to be created for the
Expand Down Expand Up @@ -57,42 +61,76 @@ pub fn ts_bindgen(
};

let world = &resolve.worlds[id];
let package = resolve
.packages
.get(
world
.package
.expect("unexpectedly missing package in world"),
)
.expect("unexpectedly missing package in world for ID");

{
let mut funcs = Vec::new();
let mut interface_imports = BTreeMap::new();
for (name, import) in world.imports.iter() {
match import {
WorldItem::Function(f) => match name {
WorldKey::Name(name) => funcs.push((name.to_string(), f)),
WorldKey::Interface(id) => funcs.push((resolve.id_of(*id).unwrap(), f)),
},
WorldItem::Interface { id, stability: _ } => match name {
WorldKey::Name(name) => {
// kebab name -> direct ns namespace import
bindgen.import_interface(resolve, name, *id, files);
WorldItem::Function(f) => {
if !feature_gate_allowed(resolve, package, &f.stability)
.expect("failed to check feature gate for imported function")
{
debug!("skipping imported function [{}] feature gate due to feature gate visibility", f.name);
continue;
}
// namespaced ns:pkg/iface
// TODO: map support
WorldKey::Interface(id) => {

match name {
WorldKey::Name(name) => funcs.push((name.to_string(), f)),
WorldKey::Interface(id) => funcs.push((resolve.id_of(*id).unwrap(), f)),
}
}
WorldItem::Interface { id, stability } => {
if !feature_gate_allowed(resolve, package, &stability)
.expect("failed to check feature gate for imported interface")
{
let import_specifier = resolve.id_of(*id).unwrap();
let (_, _, iface) = parse_world_key(&import_specifier).unwrap();
let iface = iface.to_string();
match interface_imports.entry(import_specifier) {
Entry::Vacant(entry) => {
entry.insert(vec![("*".into(), id)]);
}
Entry::Occupied(ref mut entry) => {
entry.get_mut().push((iface, id));
debug!("skipping imported interface [{}] feature gate due to feature gate visibility", iface.to_string());
continue;
}

match name {
WorldKey::Name(name) => {
// kebab name -> direct ns namespace import
bindgen.import_interface(resolve, &name, *id, files);
}
// namespaced ns:pkg/iface
// TODO: map support
WorldKey::Interface(id) => {
let import_specifier = resolve.id_of(*id).unwrap();
let (_, _, iface) = parse_world_key(&import_specifier).unwrap();
let iface = iface.to_string();
match interface_imports.entry(import_specifier) {
Entry::Vacant(entry) => {
entry.insert(vec![("*".into(), id)]);
}
Entry::Occupied(ref mut entry) => {
entry.get_mut().push((iface, id));
}
}
}
}
},
}
WorldItem::Type(tid) => {
let ty = &resolve.types[*tid];

let name = ty.name.as_ref().unwrap();

if !feature_gate_allowed(resolve, package, &ty.stability)
.expect("failed to check feature gate for imported type")
{
debug!("skipping imported type [{name}] feature gate due to feature gate visibility");
continue;
}

let mut gen = bindgen.ts_interface(resolve, true);
gen.docs(&ty.docs);
match &ty.kind {
Expand Down Expand Up @@ -134,17 +172,24 @@ pub fn ts_bindgen(
let mut funcs = Vec::new();
let mut seen_names = HashSet::new();
let mut export_aliases: Vec<(String, String)> = Vec::new();

for (name, export) in world.exports.iter() {
match export {
WorldItem::Function(f) => {
let export_name = match name {
WorldKey::Name(export_name) => export_name,
WorldKey::Interface(_) => unreachable!(),
};
if !feature_gate_allowed(resolve, package, &f.stability)
.expect("failed to check feature gate for export")
{
debug!("skipping exported interface [{export_name}] feature gate due to feature gate visibility");
continue;
}
seen_names.insert(export_name.to_string());
funcs.push((export_name.to_lower_camel_case(), f));
}
WorldItem::Interface { id, stability: _ } => {
WorldItem::Interface { id, stability } => {
let iface_id: String;
let (export_name, iface_name): (&str, &str) = match name {
WorldKey::Name(export_name) => (export_name, export_name),
Expand All @@ -154,6 +199,14 @@ pub fn ts_bindgen(
(iface_id.as_ref(), iface)
}
};

if !feature_gate_allowed(resolve, package, &stability)
.expect("failed to check feature gate for export")
{
debug!("skipping exported interface [{export_name}] feature gate due to feature gate visibility");
continue;
}

seen_names.insert(export_name.to_string());
let local_name = bindgen.export_interface(
resolve,
Expand Down Expand Up @@ -414,6 +467,14 @@ impl TsBindgen {
id: InterfaceId,
files: &mut Files,
) -> String {
let iface = resolve
.interfaces
.get(id)
.expect("unexpectedly missing interface in resolve");
let package = resolve
.packages
.get(iface.package.expect("missing package on interface"))
.expect("unexpectedly missing package");
let id_name = resolve.id_of(id).unwrap_or_else(|| name.to_string());
let goal_name = interface_goal_name(&id_name);
let goal_name_kebab = goal_name.to_kebab_case();
Expand Down Expand Up @@ -456,10 +517,16 @@ impl TsBindgen {
let mut gen = self.ts_interface(resolve, false);

uwriteln!(gen.src, "export namespace {camel} {{");

for (_, func) in resolve.interfaces[id].functions.iter() {
// Ensure that the function the world item for stability guarantees and exclude if they do not match
if !feature_gate_allowed(resolve, package, &func.stability)
.expect("failed to check feature gate for function")
{
continue;
}
gen.ts_func(func, false, true);
}
// Export resources for the interface
for (_, ty) in resolve.interfaces[id].types.iter() {
let ty = &resolve.types[*ty];
if let TypeDefKind::Resource = ty.kind {
Expand Down
18 changes: 17 additions & 1 deletion crates/wasm-tools-component/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use wit_component::{ComponentEncoder, DecodedWasm, WitPrinter};
use wit_parser::Resolve;

use exports::local::wasm_tools::tools::{
EmbedOpts, Guest, ModuleMetaType, ModuleMetadata, ProducersFields, StringEncoding,
EmbedOpts, EnabledFeatureSet, Guest, ModuleMetaType, ModuleMetadata, ProducersFields,
StringEncoding,
};

wit_bindgen::generate!({
Expand Down Expand Up @@ -74,6 +75,20 @@ impl Guest for WasmToolsJs {

let mut resolve = Resolve::default();

// Add all features specified in embed options to the resolve
// (this helps identify/use feature gating properly)
match embed_opts.features {
Some(EnabledFeatureSet::List(ref features)) => {
for f in features.into_iter() {
resolve.features.insert(f.to_string());
}
}
Some(EnabledFeatureSet::All) => {
resolve.all_features = true;
}
_ => {}
};

let ids = if let Some(wit_source) = &embed_opts.wit_source {
let path = PathBuf::from("component.wit");
resolve
Expand All @@ -89,6 +104,7 @@ impl Guest for WasmToolsJs {
};

let world_string = embed_opts.world.as_ref().map(|world| world.to_string());

let world = resolve
.select_world(&ids, world_string.as_deref())
.map_err(|e| e.to_string())?;
Expand Down
Loading

0 comments on commit 65a8f7c

Please sign in to comment.