11use anyhow:: { anyhow, bail, Context , Result } ;
22use clap:: Args ;
3+ use convert_case:: { Case , Casing } ;
34use http:: HttpAddCommand ;
45use local:: LocalAddCommand ;
56use 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
2931mod http;
3032mod 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:\n cd {}\n npm install ./{}" ,
620+ root_dir. to_string_lossy( ) ,
621+ package_name
622+ ) ;
623+
624+ Ok ( ( ) )
625+ }
626+
464627async 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+ }
0 commit comments