Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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