1use heck::*;
2use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
3use std::mem;
4use wit_bindgen_gen_core::wit_parser::abi::AbiVariant;
5use wit_bindgen_gen_core::{wit_parser::*, Direction, Files, Generator};
6
7mod gen;
8pub use gen::generate_typescript;
9
10#[derive(Default)]
11pub struct Ts {
12 src: Source,
13 imports: HashSet<String>,
14 in_import: bool,
15 opts: Opts,
16 guest_imports: HashMap<String, Imports>,
17 guest_exports: HashMap<String, Exports>,
18 sizes: SizeAlign,
19 #[allow(dead_code)]
20 needs_get_export: bool,
21 #[allow(dead_code)]
22 imported_resources: BTreeSet<ResourceId>,
23 #[allow(dead_code)]
24 exported_resources: BTreeSet<ResourceId>,
25 needs_ty_option: bool,
26 needs_ty_result: bool,
27 needs_ty_push_buffer: bool,
28 needs_ty_pull_buffer: bool,
29}
30
31#[derive(Default)]
32struct Imports {
33 freestanding_funcs: Vec<(String, Source)>,
34 resource_funcs: BTreeMap<ResourceId, Vec<(String, Source)>>,
35}
36
37#[derive(Default)]
38struct Exports {
39 freestanding_funcs: Vec<Source>,
40 arg_types: Vec<Source>,
41 resource_funcs: BTreeMap<ResourceId, Vec<Source>>,
42}
43
44#[derive(Default, Debug, Clone)]
45#[cfg_attr(feature = "structopt", derive(structopt::StructOpt))]
46pub struct Opts {
47 #[cfg_attr(feature = "structopt", structopt(long = "no-typescript"))]
48 pub no_typescript: bool,
49}
50
51impl Opts {
52 pub fn build(self) -> Ts {
53 let mut r = Ts::new();
54 r.opts = self;
55 r
56 }
57}
58
59impl Ts {
60 pub fn new() -> Ts {
61 Ts::default()
62 }
63
64 fn abi_variant(dir: Direction) -> AbiVariant {
65 match dir {
66 Direction::Import => AbiVariant::GuestExport,
67 Direction::Export => AbiVariant::GuestImport,
68 }
69 }
70
71 fn array_ty(&self, iface: &Interface, ty: &Type) -> Option<&'static str> {
72 match ty {
73 Type::U8 | Type::CChar => Some("Uint8Array"),
74 Type::S8 => Some("Int8Array"),
75 Type::U16 => Some("Uint16Array"),
76 Type::S16 => Some("Int16Array"),
77 Type::U32 | Type::Usize => Some("Uint32Array"),
78 Type::S32 => Some("Int32Array"),
79 Type::U64 => Some("BigUint64Array"),
80 Type::S64 => Some("BigInt64Array"),
81 Type::F32 => Some("Float32Array"),
82 Type::F64 => Some("Float64Array"),
83 Type::Char => None,
84 Type::Handle(_) => None,
85 Type::Id(id) => match &iface.types[*id].kind {
86 TypeDefKind::Type(t) => self.array_ty(iface, t),
87 _ => None,
88 },
89 }
90 }
91
92 fn print_ty(&mut self, iface: &Interface, ty: &Type) {
93 match ty {
94 Type::U8 => self.src.ts("u8"),
95 Type::S8 => self.src.ts("i8"),
96 Type::U16 => self.src.ts("u16"),
97 Type::S16 => self.src.ts("i16"),
98 Type::U32 => self.src.ts("u32"),
99 Type::Usize => self.src.ts("usize"),
100 Type::S32 => self.src.ts("i32"),
101 Type::F32 => self.src.ts("f32"),
102 Type::F64 => self.src.ts("f64"),
103 Type::U64 => self.src.ts("u64"),
104 Type::S64 => self.src.ts("i64"),
105 Type::CChar => self.src.ts("number"),
106 Type::Char => self.src.ts("string"),
107 Type::Handle(id) => self.src.ts(&iface.resources[*id].name.to_camel_case()),
108 Type::Id(id) => {
109 let ty = &iface.types[*id];
110 if let Some(name) = &ty.name {
111 return self.src.ts(&name.to_camel_case());
112 }
113 match &ty.kind {
114 TypeDefKind::Type(t) => self.print_ty(iface, t),
115 TypeDefKind::Record(r) if r.is_tuple() => self.print_tuple(iface, r),
116 TypeDefKind::Record(_) => panic!("anonymous record"),
117 TypeDefKind::Variant(v) if v.is_bool() => self.src.ts("boolean"),
118 TypeDefKind::Variant(v) => {
119 if iface.is_nullable_option(v) {
120 self.print_ty(iface, v.cases[1].ty.as_ref().unwrap());
121 self.src.ts(" | null");
122 } else if let Some(t) = v.as_option() {
123 self.needs_ty_option = true;
124 self.src.ts("Option<");
125 self.print_ty(iface, t);
126 self.src.ts(">");
127 } else if let Some((ok, err)) = v.as_expected() {
128 self.needs_ty_result = true;
129 self.src.ts("Result<");
130 match ok {
131 Some(ok) => self.print_ty(iface, ok),
132 None => self.src.ts("undefined"),
133 }
134 self.src.ts(", ");
135 match err {
136 Some(err) => self.print_ty(iface, err),
137 None => self.src.ts("undefined"),
138 }
139 self.src.ts(">");
140 } else {
141 panic!("anonymous variant");
142 }
143 }
144 TypeDefKind::List(v) => self.print_list(iface, v),
145 TypeDefKind::PushBuffer(v) => self.print_buffer(iface, true, v),
146 TypeDefKind::PullBuffer(v) => self.print_buffer(iface, false, v),
147 TypeDefKind::Pointer(_) | TypeDefKind::ConstPointer(_) => {
148 self.src.ts("number");
149 }
150 }
151 }
152 }
153 }
154
155 fn print_list(&mut self, iface: &Interface, ty: &Type) {
156 if let Some(r) = self.hash_map(iface, ty) {
157 self.src.ts("Record<");
158 self.print_ty(iface, &r.fields[0].ty);
159 self.src.ts(", ");
160 self.print_ty(iface, &r.fields[1].ty);
161 self.src.ts(">");
162 } else {
163 match self.array_ty(iface, ty) {
164 Some(ty) => self.src.ts(ty),
165 None => {
166 if let Type::Char = ty {
167 self.src.ts("string");
168 } else {
169 self.print_ty(iface, ty);
170 self.src.ts("[]");
171 }
172 }
173 }
174 }
175 }
176
177 fn print_tuple(&mut self, iface: &Interface, record: &Record) {
178 self.src.ts("[");
179 for (i, field) in record.fields.iter().enumerate() {
180 if i > 0 {
181 self.src.ts(", ");
182 }
183 self.print_ty(iface, &field.ty);
184 }
185 self.src.ts("]");
186 }
187
188 fn print_buffer(&mut self, iface: &Interface, push: bool, ty: &Type) {
189 match self.array_ty(iface, ty) {
190 Some(ty) => self.src.ts(ty),
191 None => {
192 if push {
193 self.needs_ty_push_buffer = true;
194 self.src.ts("PushBuffer");
195 } else {
196 self.needs_ty_pull_buffer = true;
197 self.src.ts("PullBuffer");
198 }
199 self.src.ts("<");
200 self.print_ty(iface, ty);
201 self.src.ts(">");
202 }
203 }
204 }
205
206 fn docs(&mut self, docs: &Docs) {
207 self.doc_str(&docs.contents);
208 }
209
210 fn doc_str(&mut self, contents: &Option<String>) {
211 let docs = match &contents {
212 Some(docs) => docs,
213 None => return,
214 };
215 let lines = docs
216 .lines()
217 .filter(|line| *line != "@change" && *line != "@view")
218 .collect::<Vec<&str>>();
219 if !lines.is_empty() {
220 self.src.ts("/**\n");
221 for line in lines {
222 self.src.ts(&format!("* {}\n", line));
223 }
224 self.src.ts("*/\n");
225 }
226 }
227
228 fn print_args_type(&mut self, iface: &Interface, func: &Function, param_start: usize) {
229 let none: Option<String> = None;
230 let arg_fields: Vec<(&str, &Type, &Option<String>)> = func.params[param_start..]
231 .iter()
232 .map(|(name, ty)| (name.as_str(), ty, &none))
233 .collect();
234 self.print_fields(iface, arg_fields);
235 }
236 #[allow(dead_code)]
237 fn print_args(&mut self, iface: &Interface, func: &Function, param_start: usize) {
238 self.src.ts("(args");
239 if func.params.is_empty() {
240 self.src.ts(" = {}");
241 } else {
242 self.src.ts(": {\n");
243 self.print_args_type(iface, func, param_start);
244 self.src.ts("}");
245 }
246 if func.params.is_empty() {};
247 self.src.ts(", options?: ");
248 self.src.ts(if is_change(func) {
249 "ChangeMethodOptions"
250 } else {
251 "ViewFunctionOptions"
252 });
253 self.src.ts("): ");
254 }
255
256 fn ts_func(&mut self, _iface: &Interface, _func: &Function) {
257 }
325
326 fn print_func_result(&mut self, iface: &Interface, func: &Function) {
327 match func.results.len() {
328 0 => self.src.ts("void"),
329 1 => self.print_ty(iface, &func.results[0].1),
330 _ => {
331 if func.results.iter().any(|(n, _)| n.is_empty()) {
332 self.src.ts("[");
333 for (i, (_, ty)) in func.results.iter().enumerate() {
334 if i > 0 {
335 self.src.ts(", ");
336 }
337 self.print_ty(iface, ty);
338 }
339 self.src.ts("]");
340 } else {
341 self.src.ts("{ ");
342 for (i, (name, ty)) in func.results.iter().enumerate() {
343 if i > 0 {
344 self.src.ts(", ");
345 }
346 self.src.ts(&name.to_mixed_case());
347 self.src.ts(": ");
348 self.print_ty(iface, ty);
349 }
350 self.src.ts(" }");
351 }
352 }
353 }
354 }
355
356 fn print_fields(&mut self, iface: &Interface, fields: Vec<(&str, &Type, &Option<String>)>) {
357 for (name, ty, docs) in fields.iter() {
358 self.doc_str(docs);
359 self.src.ts(&name.to_snake_case());
360 if iface.is_ty_nullable_option(ty) {
361 self.src.ts("?");
362 }
363 self.src.ts(": ");
364 let ty = iface.get_nullable_option(ty).unwrap_or(ty);
365 self.print_ty(iface, ty);
366 self.src.ts(";\n");
367 }
368 }
369}
370
371impl Generator for Ts {
372 fn preprocess_one(&mut self, iface: &Interface, dir: Direction) {
373 let variant = Self::abi_variant(dir);
374 self.sizes.fill(variant, iface);
375 self.in_import = variant == AbiVariant::GuestImport;
376 self.add_preamble()
377 }
378
379 fn type_record(
380 &mut self,
381 iface: &Interface,
382 _id: TypeId,
383 name: &str,
384 record: &Record,
385 docs: &Docs,
386 ) {
387 self.docs(docs);
388 self.imports.insert(name.to_camel_case());
389 if record.is_tuple() {
390 self.src
391 .ts(&format!("export type {} = ", name.to_camel_case()));
392 self.print_tuple(iface, record);
393 self.src.ts(";\n");
394 } else if record.is_flags() {
395 let repr = iface
396 .flags_repr(record)
397 .expect("unsupported number of flags");
398 let suffix = if repr == Int::U64 {
399 self.src
400 .ts(&format!("export type {} = u64;\n", name.to_camel_case()));
401 "n"
402 } else {
403 self.src
404 .ts(&format!("export type {} = number;\n", name.to_camel_case()));
405 ""
406 };
407 let name = name.to_shouty_snake_case();
408 for (i, field) in record.fields.iter().enumerate() {
409 let field = field.name.to_shouty_snake_case();
410 self.src.ts(&format!(
411 "export const {}_{} = {}{};\n",
412 name,
413 field,
414 1u64 << i,
415 suffix,
416 ));
417 }
418 } else {
419 self.src
420 .ts(&format!("export interface {} {{\n", name.to_camel_case()));
421
422 let fields = record
423 .fields
424 .iter()
425 .map(|f| (f.name.as_str(), &f.ty, &f.docs.contents))
426 .collect();
427 self.print_fields(iface, fields);
428 self.src.ts("}\n");
429 }
430 }
431
432 fn type_variant(
433 &mut self,
434 iface: &Interface,
435 _id: TypeId,
436 name: &str,
437 variant: &Variant,
438 docs: &Docs,
439 ) {
440 self.docs(docs);
441 self.imports.insert(name.to_camel_case());
442 if variant.is_bool() {
443 self.src.ts(&format!(
444 "export type {} = boolean;\n",
445 name.to_camel_case(),
446 ));
447 } else if iface.is_nullable_option(variant) {
448 self.src
449 .ts(&format!("export type {} = ", name.to_camel_case()));
450 self.print_ty(iface, variant.cases[1].ty.as_ref().unwrap());
451 self.src.ts(" | null;\n");
452 } else if variant.is_enum() {
453 self.src
454 .ts(&format!("export enum {} {{\n", name.to_camel_case()));
455 for case in variant.cases.iter() {
456 self.docs(&case.docs);
457 let name = case.name.to_camel_case();
458 self.src.ts(&format!("{} = \"{}\",\n", name, name));
459 }
460 self.src.ts("}\n");
461 } else {
462 self.src
463 .ts(&format!("export type {} = ", name.to_camel_case()));
464 for (i, case) in variant.cases.iter().enumerate() {
465 if i > 0 {
466 self.src.ts(" | ");
467 }
468 self.src
469 .ts(&format!("{}_{}", name, case.name).to_camel_case());
470 }
471 self.src.ts(";\n");
472 for case in variant.cases.iter() {
473 self.docs(&case.docs);
474 self.src.ts(&format!(
475 "export interface {} {{\n",
476 format!("{}_{}", name, case.name).to_camel_case()
477 ));
478 self.src.ts("tag: \"");
479 self.src.ts(&case.name);
480 self.src.ts("\",\n");
481 if let Some(ty) = &case.ty {
482 self.src.ts("val: ");
483 self.print_ty(iface, ty);
484 self.src.ts(",\n");
485 }
486 self.src.ts("}\n");
487 }
488 }
489 }
490
491 fn type_resource(&mut self, _iface: &Interface, ty: ResourceId) {
492 if !self.in_import {
493 self.exported_resources.insert(ty);
494 }
495 }
496
497 fn type_alias(&mut self, iface: &Interface, _id: TypeId, name: &str, ty: &Type, docs: &Docs) {
498 self.docs(docs);
499 self.src
500 .ts(&format!("export type {} = ", name.to_camel_case()));
501 self.imports.insert(name.to_camel_case());
502 self.print_ty(iface, ty);
503 self.src.ts(";\n");
504 }
505
506 fn type_list(&mut self, iface: &Interface, _id: TypeId, name: &str, ty: &Type, docs: &Docs) {
507 self.docs(docs);
508 self.src
509 .ts(&format!("export type {} = ", name.to_camel_case()));
510 self.imports.insert(name.to_camel_case());
511 self.print_list(iface, ty);
512 self.src.ts(";\n");
513 }
514
515 fn type_pointer(
516 &mut self,
517 iface: &Interface,
518 _id: TypeId,
519 name: &str,
520 const_: bool,
521 ty: &Type,
522 docs: &Docs,
523 ) {
524 #[allow(clippy::drop_copy)]
525 drop((iface, _id, name, const_, ty, docs));
526 }
527
528 fn type_builtin(&mut self, iface: &Interface, _id: TypeId, name: &str, ty: &Type, docs: &Docs) {
529 #[allow(clippy::drop_copy)]
530 drop((iface, _id, name, ty, docs));
531 }
532
533 fn type_push_buffer(
534 &mut self,
535 iface: &Interface,
536 _id: TypeId,
537 name: &str,
538 ty: &Type,
539 docs: &Docs,
540 ) {
541 self.docs(docs);
542 self.src
543 .ts(&format!("export type {} = ", name.to_camel_case()));
544 self.print_buffer(iface, true, ty);
545 self.src.ts(";\n");
546 }
547
548 fn type_pull_buffer(
549 &mut self,
550 iface: &Interface,
551 _id: TypeId,
552 name: &str,
553 ty: &Type,
554 docs: &Docs,
555 ) {
556 self.docs(docs);
557 self.src
558 .ts(&format!("export type {} = ", name.to_camel_case()));
559 self.print_buffer(iface, false, ty);
560 self.src.ts(";\n");
561 }
562
563 #[allow(dead_code, unused_variables)]
567 fn export(&mut self, iface: &Interface, func: &Function) {}
568
569 fn import(&mut self, iface: &Interface, func: &Function) {
573 let prev = mem::take(&mut self.src);
574 self.ts_func(iface, func);
575
576 let exports = self
577 .guest_exports
578 .entry(iface.name.to_string())
579 .or_insert_with(Exports::default);
580
581 let func_body = mem::replace(&mut self.src, prev);
582 match &func.kind {
583 FunctionKind::Freestanding => {
584 exports.freestanding_funcs.push(func_body);
585 }
586 FunctionKind::Static { resource, .. } | FunctionKind::Method { resource, .. } => {
587 exports
588 .resource_funcs
589 .entry(*resource)
590 .or_insert_with(Vec::new)
591 .push(func_body);
592 }
593 };
594 let prev = mem::take(&mut self.src);
595 let is_change_func = is_change(func);
596 let func_type = if is_change_func { "change" } else { "view" };
597 let docs = func
598 .docs
599 .contents
600 .as_ref()
601 .map(|d| d.to_string())
602 .unwrap_or_default();
603 self.doc_str(&Some(format!("{docs}\n@contractMethod {func_type}\n")));
604 self.src.ts(&format!(
605 "export interface {} {{\n",
606 func.name.to_camel_case()
607 ));
608
609 self.src.ts("args: {");
610 if !func.params.is_empty() {
611 self.src.ts("\n");
612 self.print_args_type(iface, func, 0);
613 }
614 self.src.ts("};\n");
615 if is_change_func {
616 self.src.ts("options: CallOptions\n");
617 }
618 self.src.ts("\n}\n");
619 self.src.ts(&format!(
620 "export type {}__Result = ",
621 func.name.to_camel_case()
622 ));
623 self.print_func_result(iface, func);
624 self.src.ts(";\n");
625
626 let func_args = mem::replace(&mut self.src, prev);
627 let exports = self
628 .guest_exports
629 .entry(iface.name.to_string())
630 .or_insert_with(Exports::default);
631
632 exports.arg_types.push(func_args);
633 }
634
635 fn finish_one(&mut self, iface: &Interface, files: &mut Files) {
636 for (module, funcs) in mem::take(&mut self.guest_imports) {
637 self.src
638 .ts(&format!("export interface {} {{\n", module.to_camel_case()));
639
640 for (_, src) in funcs.freestanding_funcs.iter() {
641 self.src.ts(&src.ts);
642 }
643
644 self.src.ts("}\n");
645
646 for (resource, _) in iface.resources.iter() {
647 self.src.ts(&format!(
648 "export interface {} {{\n",
649 iface.resources[resource].name.to_camel_case()
650 ));
651 if let Some(funcs) = funcs.resource_funcs.get(&resource) {
652 for (_, src) in funcs {
653 self.src.ts(&src.ts);
654 }
655 }
656 self.src.ts("}\n");
657 }
658 }
659 let imports = mem::take(&mut self.src);
660 let mut main = wit_bindgen_gen_core::Source::default();
661 for (_module, exports) in mem::take(&mut self.guest_exports) {
662 for func in exports.freestanding_funcs.iter() {
666 self.src.ts(&func.ts);
667 }
668 for args in exports.arg_types.iter() {
670 main.push_str(&args.ts);
671 }
672 }
673
674 if mem::take(&mut self.needs_ty_option) {
675 self.imports.insert("Option".to_string());
676 self.src
677 .ts("export type Option<T> = { tag: \"none\" } | { tag: \"some\", val; T };\n");
678 }
679 if mem::take(&mut self.needs_ty_result) {
680 self.imports.insert("Result".to_string());
681 self.src.ts(
682 "export type Result<T, E> = { tag: \"ok\", val: T } | { tag: \"err\", val: E };\n",
683 );
684 }
685 let exports = mem::take(&mut self.src);
686
687 self.src.ts(&imports.ts);
688 self.src.ts(&exports.ts);
689
690 self.src.main(&format!(
691 r#"
692import {{
693 u8,
694 i8,
695 u16,
696 i16,
697 u32,
698 i32,
699 u64,
700 i64,
701 f32,
702 f64,
703 CallOptions,
704 {},
705}} from "./types";
706
707"#,
708 self.imports
709 .iter()
710 .cloned()
711 .collect::<Vec<String>>()
712 .join(",\n\t")
713 ));
714 self.src.main(&main);
715
716 let src = mem::take(&mut self.src);
717 let name = iface.name.to_kebab_case();
718 files.push("types.ts", src.ts.as_bytes());
719 files.push(&format!("{}.ts", name), src.main.as_bytes());
720 }
721
722 fn finish_all(&mut self, _files: &mut Files) {
723 assert!(self.src.ts.is_empty());
724 }
725}
726
727impl Ts {
728 fn hash_map<'a>(&self, iface: &'a Interface, ty: &Type) -> Option<&'a Record> {
729 iface
730 .get_record(ty)
731 .filter(|r| r.is_tuple() && r.fields.len() == 2)
732 }
733 fn add_preamble(&mut self) {
734 self.src.ts(HELPER);
735 self.src.ts("\n\n");
736 }
737}
738
739pub fn to_js_ident(name: &str) -> &str {
740 match name {
741 "in" => "in_",
742 "import" => "import_",
743 s => s,
744 }
745}
746
747#[derive(Default)]
748struct Source {
749 main: wit_bindgen_gen_core::Source,
750 ts: wit_bindgen_gen_core::Source,
751}
752
753impl Source {
754 fn ts(&mut self, s: &str) {
755 self.ts.push_str(s);
756 }
757
758 fn main(&mut self, s: &str) {
759 self.main.push_str(s);
760 }
761}
762
763fn is_change(func: &Function) -> bool {
764 if let Some(docs) = &func.docs.contents {
765 let mut x = docs.split('\n').filter(|s| *s == "@change").peekable();
766 if x.peek().is_some() {
767 return true;
768 }
769 }
770 false
771}
772
773const HELPER: &str = "
774/**
775* @minimum 0
776* @maximum 18446744073709551615
777* @asType integer
778*/
779export type u64 = number;
780/**
781* @minimum -9223372036854775808
782* @maximum 9223372036854775807
783* @asType integer
784*/
785export type i64 = number;
786
787/**
788* @minimum 0
789* @maximum 255
790* @asType integer
791* */
792export type u8 = number;
793/**
794* @minimum -128
795* @maximum 127
796* @asType integer
797* */
798export type i8 = number;
799/**
800* @minimum 0
801* @maximum 65535
802* @asType integer
803* */
804export type u16 = number;
805/**
806* @minimum -32768
807* @maximum 32767
808* @asType integer
809* */
810export type i16 = number;
811/**
812* @minimum 0
813* @maximum 4294967295
814* @asType integer
815* */
816export type u32 = number;
817/**
818* @minimum 0
819* @maximum 4294967295
820* @asType integer
821* */
822export type usize = number;
823/**
824* @minimum -2147483648
825* @maximum 2147483647
826* @asType integer
827* */
828export type i32 = number;
829
830/**
831* @minimum -3.40282347E+38
832* @maximum 3.40282347E+38
833*/
834export type f32 = number;
835
836/**
837* @minimum -1.7976931348623157E+308
838* @maximum 1.7976931348623157E+308
839*/
840export type f64 = number;
841
842export type CallOptions = {
843 /** Units in gas
844 * @pattern [0-9]+
845 * @default \"30000000000000\"
846 */
847 gas?: string;
848 /** Units in yoctoNear
849 * @default \"0\"
850 */
851 attachedDeposit?: Balance;
852}
853";