Skip to content

Commit aa8e0df

Browse files
committed
initial support for js sdk v4 bindings generation
Signed-off-by: karthik2804 <[email protected]>
1 parent 37528eb commit aa8e0df

File tree

8 files changed

+669
-361
lines changed

8 files changed

+669
-361
lines changed

Cargo.lock

Lines changed: 447 additions & 251 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ tokio = { version = "1.40.0", features = ["full"] }
1919
toml = "0.8.19"
2020
toml_edit = "0.22.21"
2121
url = "2.5.2"
22-
wasmparser = { git = "https://github.com/bytecodealliance/wasm-tools" }
23-
wit-component = { git = "https://github.com/bytecodealliance/wasm-tools" }
24-
wit-parser = { git = "https://github.com/bytecodealliance/wasm-tools" }
22+
wasmparser = "0.227.1"
23+
wit-component = "0.227.1"
24+
wit-parser = "0.227.1"
2525
futures = "0.3.30"
26-
semver = "1.0.23"
27-
wit-bindgen-rust = { git = "https://github.com/fibonacci1729/wit-bindgen", branch = "deps" }
28-
wit-bindgen-core = { git = "https://github.com/fibonacci1729/wit-bindgen", branch = "deps" }
26+
semver = "1.0.25"
27+
wit-bindgen-rust = "0.41.0"
28+
wit-bindgen-core = "0.41.0"
2929
wasm-pkg-common = "0.5.1"
3030
wasm-pkg-client = "0.5.1"
31+
js-component-bindgen = { git = "https://github.com/bytecodealliance/jco", rev = "48c1a3c91a9c71d35aedc9572e180ce67ca3a4f5" }
32+
convert_case = "0.8.0"
3133

3234
[target.'cfg(target_os = "linux")'.dependencies]
3335
# This needs to be an explicit dependency to enable

src/commands/add.rs

Lines changed: 211 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::{anyhow, bail, Context, Result};
22
use clap::Args;
3+
use convert_case::{Case, Casing};
34
use http::HttpAddCommand;
45
use local::LocalAddCommand;
56
use registry::RegistryAddCommand;
@@ -25,6 +26,7 @@ use crate::common::{
2526
paths::fs_safe_segment,
2627
wit::{get_exported_interfaces, parse_component_bytes, resolve_to_wit},
2728
};
29+
use js_component_bindgen::{generate_types, TranspileOpts};
2830

2931
mod http;
3032
mod local;
@@ -198,6 +200,7 @@ impl AddCommand {
198200
root_dir,
199201
target_component,
200202
package_name: package,
203+
resolve: &resolve,
201204
interfaces: &interfaces,
202205
rel_wit_path: &output_wit_path,
203206
};
@@ -402,6 +405,7 @@ struct BindOMatic<'a> {
402405
root_dir: &'a Path,
403406
target_component: &'a spin_manifest::schema::v2::Component,
404407
package_name: &'a wit_parser::PackageName,
408+
resolve: &'a wit_parser::Resolve,
405409
interfaces: &'a [String],
406410
rel_wit_path: &'a Path,
407411
}
@@ -457,10 +461,169 @@ async fn try_generate_bindings<'a>(target: &'a BindOMatic<'a>) -> anyhow::Result
457461
)
458462
.await
459463
}
460-
Language::TypeScript { package_json: _ } => todo!(),
464+
Language::TypeScript { package_json: _ } => {
465+
generate_ts_bindings(
466+
target.root_dir,
467+
target.package_name,
468+
&mut target.resolve.clone(),
469+
)
470+
.await
471+
}
461472
}
462473
}
463474

475+
async fn generate_ts_bindings(
476+
root_dir: &Path,
477+
package_name: &wit_parser::PackageName,
478+
resolve: &mut Resolve,
479+
) -> anyhow::Result<()> {
480+
println!(
481+
"Generating TypeScript bindings for {}/{}",
482+
package_name.namespace, package_name.name
483+
);
484+
485+
let package_name_str = if let Some(v) = &package_name.version {
486+
format!(
487+
"@spin-deps/{}-{}@{}",
488+
package_name.namespace, package_name.name, v
489+
)
490+
} else {
491+
format!(
492+
"@spin-deps/{}-{}",
493+
package_name.namespace, package_name.name
494+
)
495+
};
496+
497+
let package_id = resolve
498+
.packages
499+
.iter()
500+
.find(|(_, p)| &p.name.to_string() == "root:component")
501+
.unwrap()
502+
.0;
503+
504+
let world_id = resolve.select_world(package_id, Some("root"))?;
505+
506+
let out_world_name = &package_name
507+
.to_string()
508+
.replace("_", "-")
509+
.replace(":", "-")
510+
.replace("@", "")
511+
.replace("/", "-");
512+
513+
resolve.importize(world_id, Some(out_world_name.clone()))?;
514+
let out_world_id = resolve.select_world(package_id, Some(&out_world_name))?;
515+
516+
// Create a new directory within the spin component working directory
517+
let package_dir = root_dir.join(&package_name_str);
518+
fs::create_dir_all(&package_dir).await?;
519+
520+
// add a package.json file
521+
let package_json = package_dir.join("package.json");
522+
let package_json_content = package_json_content(&package_name_str, &out_world_name);
523+
fs::write(&package_json, package_json_content)
524+
.await
525+
.context("no package json file")?;
526+
// create tsconfig
527+
let tsconfig = package_dir.join("tsconfig.json");
528+
let tsconfig_content = tsconfig_content();
529+
fs::write(&tsconfig, tsconfig_content)
530+
.await
531+
.context("no tsconfig file")?;
532+
// write the wit from the resolve in wit/world.wit
533+
let world_wit = package_dir.join("wit/world.wit");
534+
// create if not exist
535+
fs::create_dir_all(world_wit.parent().unwrap()).await?;
536+
let world_wit_text = resolve_to_wit(resolve, package_id).context("failed to resolve to wit")?;
537+
fs::write(&world_wit, world_wit_text)
538+
.await
539+
.context("No wit folder")?;
540+
541+
let files = generate_types(
542+
// This name does not matter as we are not going to use it
543+
"test",
544+
resolve.clone(),
545+
world_id,
546+
TranspileOpts {
547+
name: package_name_str.clone(),
548+
no_typescript: false,
549+
instantiation: None,
550+
import_bindings: None,
551+
map: None,
552+
no_nodejs_compat: false,
553+
base64_cutoff: 0,
554+
async_mode: None,
555+
tla_compat: false,
556+
valid_lifting_optimization: false,
557+
tracing: false,
558+
no_namespaced_exports: false,
559+
multi_memory: true,
560+
guest: true,
561+
},
562+
)?;
563+
564+
for (name, contents) in files.iter() {
565+
let output_path = package_dir.join("types").join(name);
566+
// Create parent directories if they don't exist
567+
if let Some(parent) = output_path.parent() {
568+
fs::create_dir_all(parent).await?;
569+
}
570+
fs::write(output_path, contents).await?;
571+
}
572+
// for all interface names in interfaces, import and re-export them in a index.js file
573+
let mut re_exports: Vec<String> = Vec::new();
574+
let mut name_counts: HashMap<String, usize> = HashMap::new();
575+
for (_, item) in resolve.worlds[out_world_id].imports.iter() {
576+
match item {
577+
wit_parser::WorldItem::Interface { id, stability: _ } => {
578+
let iface = &resolve.interfaces[*id];
579+
580+
let iface_name = &iface.name.clone().unwrap().to_case(Case::Camel);
581+
let package = &resolve.packages[iface.package.unwrap()];
582+
// Only handle interfaces from the package we are generating bindings for
583+
if package.name != *package_name {
584+
continue;
585+
}
586+
587+
// Track names to detect collision
588+
let count = name_counts.entry(iface_name.clone()).or_insert(0);
589+
*count += 1;
590+
591+
let final_name = if *count > 1 {
592+
format!("{}{}", package_name, iface_name)
593+
} else {
594+
iface_name.clone()
595+
};
596+
597+
let import_path = qualified_itf_name(&package.name, &iface.name.clone().unwrap());
598+
599+
re_exports.push(format!(
600+
"import * as {} from '{}';",
601+
final_name, import_path
602+
));
603+
re_exports.push(format!("export {{ {} }};", final_name));
604+
605+
println!("import * as {} from '{}';", final_name, import_path);
606+
println!("export {{ {} }};", final_name);
607+
}
608+
// TODO: spin deps itself does not importing functions
609+
wit_parser::WorldItem::Function(_) => {}
610+
// Types are not generated by the TypeScript bindings generator
611+
wit_parser::WorldItem::Type(_) => {}
612+
}
613+
}
614+
let index_js = package_dir.join("index.js");
615+
fs::write(&index_js, re_exports.join("\n")).await?;
616+
617+
println!("TypeScript bindings generated successfully");
618+
println!(
619+
"To use the component, run:\ncd {}\n npm install ./{}",
620+
root_dir.to_string_lossy(),
621+
package_name
622+
);
623+
624+
Ok(())
625+
}
626+
464627
async fn generate_rust_bindings(
465628
root_dir: &Path,
466629
package_name: &wit_parser::PackageName,
@@ -556,3 +719,50 @@ async fn generate_rust_bindings(
556719

557720
Ok(())
558721
}
722+
723+
fn package_json_content(package_name: &str, world: &str) -> String {
724+
format!(
725+
r#"{{
726+
"name": "{package_name}",
727+
"version": "0.1.0",
728+
"description": "Generated Package for {package_name}",
729+
"main": "index.js",
730+
"scripts": {{
731+
}},
732+
"author": "",
733+
"license": "ISC",
734+
"config": {{
735+
"witDeps":
736+
[
737+
{{
738+
"witPath": "./wit",
739+
"package": "root:component",
740+
"world": "{world}"
741+
}}
742+
]
743+
}}
744+
}}"#
745+
)
746+
}
747+
748+
fn tsconfig_content() -> String {
749+
r#"{
750+
"compilerOptions": {
751+
"target": "ES2020",
752+
"module": "ES2020",
753+
"lib": [
754+
"ES2020"
755+
],
756+
"moduleResolution": "node",
757+
"declaration": true,
758+
"outDir": "dist",
759+
"strict": true,
760+
"esModuleInterop": true,
761+
},
762+
"exclude": [
763+
"node_modules",
764+
"dist"
765+
]
766+
}"#
767+
.to_owned()
768+
}

src/commands/bindings.rs

Lines changed: 0 additions & 96 deletions
This file was deleted.

src/commands/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
pub mod add;
2-
pub mod bindings;
32
pub mod publish;

src/common/constants.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
pub const SPIN_WIT_DIRECTORY: &str = ".wit/components";
2-
pub const SPIN_DEPS_WIT_FILE_NAME: &str = "deps.wit";

src/common/wit.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ pub fn resolve_to_wit(resolve: &Resolve, package_id: PackageId) -> Result<String
1414
.filter(|id| *id != package_id)
1515
.collect::<Vec<_>>();
1616

17-
printer.print(resolve, package_id, &ids)
17+
printer.print(resolve, package_id, &ids)?;
18+
Ok(printer.output.to_string())
1819
}
1920

2021
pub fn parse_component_bytes(bytes: Vec<u8>) -> Result<(Resolve, PackageId)> {

0 commit comments

Comments
 (0)