Skip to content

Commit

Permalink
feat: support semver aware exclusions (#459)
Browse files Browse the repository at this point in the history
  • Loading branch information
vados-cosmonic authored Aug 19, 2024
1 parent 3720a5c commit d07be8f
Show file tree
Hide file tree
Showing 18 changed files with 611 additions and 61 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 }
43 changes: 39 additions & 4 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 @@ -67,7 +67,8 @@ pub fn generate_types(
) -> Result<Vec<(String, Vec<u8>)>, anyhow::Error> {
let mut files = files::Files::default();

ts_bindgen(&name, &resolve, world_id, &opts, &mut files);
ts_bindgen(&name, &resolve, world_id, &opts, &mut files)
.context("failed to generate Typescript bindings")?;

let mut files_out: Vec<(String, Vec<u8>)> = Vec::new();
for (name, source) in files.iter() {
Expand Down Expand Up @@ -137,7 +138,8 @@ pub fn transpile(component: &[u8], opts: TranspileOpts) -> Result<Transpiled, an
}

if !opts.no_typescript {
ts_bindgen(&name, &resolve, world_id, &opts, &mut files);
ts_bindgen(&name, &resolve, world_id, &opts, &mut files)
.context("failed to generate Typescript bindings")?;
}

let (imports, exports) = transpile_bindgen(
Expand Down Expand Up @@ -172,3 +174,36 @@ 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,
item_name: &str,
) -> 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 gate on [{item_name}] refers to an unreleased (future) package version [{since}] (current package version is [{package_version}])"
);

// 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)
}
})
}
124 changes: 99 additions & 25 deletions crates/js-component-bindgen/src/ts_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ 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 anyhow::{Context as _, Result};
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 @@ -47,7 +52,7 @@ pub fn ts_bindgen(
id: WorldId,
opts: &TranspileOpts,
files: &mut Files,
) {
) -> Result<()> {
let mut bindgen = TsBindgen {
src: Source::default(),
interface_names: LocalNames::default(),
Expand All @@ -57,42 +62,81 @@ pub fn ts_bindgen(
};

let world = &resolve.worlds[id];
let package = resolve
.packages
.get(
world
.package
.context("unexpectedly missing package in world")?,
)
.context("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, &f.name)
.context("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 } => {
let iface_name = &resolve.interfaces[*id]
.name
.as_ref()
.map(String::as_str)
.unwrap_or("<unnamed>");
if !feature_gate_allowed(resolve, package, &stability, iface_name)
.context("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, name)
.context("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 +178,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, &f.name)
.context("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 +205,14 @@ pub fn ts_bindgen(
(iface_id.as_ref(), iface)
}
};

if !feature_gate_allowed(resolve, package, &stability, iface_name)
.context("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 @@ -298,6 +357,7 @@ pub fn ts_bindgen(
}

files.push(&format!("{name}.d.ts"), bindgen.src.as_bytes());
Ok(())
}

impl TsBindgen {
Expand Down Expand Up @@ -414,6 +474,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 +524,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, &func.name)
.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
Loading

0 comments on commit d07be8f

Please sign in to comment.