diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 303618c1..53f20c11 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -221,6 +221,7 @@ pub struct Function { pub rust_attrs: Vec, pub rust_vis: syn::Visibility, pub r#async: bool, + pub generate_typescript: bool, } #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] @@ -231,6 +232,7 @@ pub struct Struct { pub fields: Vec, pub comments: Vec, pub is_inspectable: bool, + pub generate_typescript: bool, } #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] @@ -243,6 +245,7 @@ pub struct StructField { pub getter: Ident, pub setter: Ident, pub comments: Vec, + pub generate_typescript: bool, } #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] @@ -252,6 +255,7 @@ pub struct Enum { pub variants: Vec, pub comments: Vec, pub hole: u32, + pub generate_typescript: bool, } #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs index d60e34c6..67e6bd64 100644 --- a/crates/backend/src/encode.rs +++ b/crates/backend/src/encode.rs @@ -206,6 +206,7 @@ fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Functi Function { arg_names, name: &func.name, + generate_typescript: func.generate_typescript, } } @@ -218,6 +219,7 @@ fn shared_enum<'a>(e: &'a ast::Enum, intern: &'a Interner) -> Enum<'a> { .map(|v| shared_variant(v, intern)) .collect(), comments: e.comments.iter().map(|s| &**s).collect(), + generate_typescript: e.generate_typescript, } } @@ -307,6 +309,7 @@ fn shared_struct<'a>(s: &'a ast::Struct, intern: &'a Interner) -> Struct<'a> { .collect(), comments: s.comments.iter().map(|s| &**s).collect(), is_inspectable: s.is_inspectable, + generate_typescript: s.generate_typescript, } } @@ -318,6 +321,7 @@ fn shared_struct_field<'a>(s: &'a ast::StructField, intern: &'a Interner) -> Str }, readonly: s.readonly, comments: s.comments.iter().map(|s| &**s).collect(), + generate_typescript: s.generate_typescript, } } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 4e6bb310..4b375e4b 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -111,7 +111,7 @@ impl<'a> Context<'a> { &mut self, export_name: &str, contents: &str, - comments: Option, + comments: Option<&str>, ) -> Result<(), Error> { let definition_name = generate_identifier(export_name, &mut self.defined_identifiers); if contents.starts_with("class") && definition_name != export_name { @@ -119,9 +119,8 @@ impl<'a> Context<'a> { } let contents = contents.trim(); - if let Some(ref c) = comments { + if let Some(c) = comments { self.globals.push_str(c); - self.typescript.push_str(c); } let global = match self.config.mode { OutputMode::Node { @@ -804,7 +803,7 @@ impl<'a> Context<'a> { dst.push_str("}\n"); ts_dst.push_str("}\n"); - self.export(&name, &dst, Some(class.comments.clone()))?; + self.export(&name, &dst, Some(&class.comments))?; self.typescript.push_str(&ts_dst); Ok(()) @@ -2153,15 +2152,23 @@ impl<'a> Context<'a> { // on what's being exported. match kind { Kind::Export(export) => { + let ts_sig = match export.generate_typescript { + true => Some(ts_sig.as_str()), + false => None, + }; + let docs = format_doc_comments(&export.comments, Some(js_doc)); match &export.kind { AuxExportKind::Function(name) => { - self.export(&name, &format!("function{}", code), Some(docs))?; + if let Some(ts_sig) = ts_sig { + self.typescript.push_str(&docs); + self.typescript.push_str("export function "); + self.typescript.push_str(&name); + self.typescript.push_str(ts_sig); + self.typescript.push_str(";\n"); + } + self.export(&name, &format!("function{}", code), Some(&docs))?; self.globals.push_str("\n"); - self.typescript.push_str("export function "); - self.typescript.push_str(&name); - self.typescript.push_str(&ts_sig); - self.typescript.push_str(";\n"); } AuxExportKind::Constructor(class) => { let exported = require_class(&mut self.exported_classes, class); @@ -2169,25 +2176,34 @@ impl<'a> Context<'a> { bail!("found duplicate constructor for class `{}`", class); } exported.has_constructor = true; - exported.push(&docs, "constructor", "", &code, &ts_sig); + exported.push(&docs, "constructor", "", &code, ts_sig); } AuxExportKind::Getter { class, field } => { - let ret_ty = ts_ret_ty.unwrap(); + let ret_ty = match export.generate_typescript { + true => match &ts_ret_ty { + Some(s) => Some(s.as_str()), + _ => None, + }, + false => None, + }; let exported = require_class(&mut self.exported_classes, class); - exported.push_getter(&docs, field, &code, &ret_ty); + exported.push_getter(&docs, field, &code, ret_ty); } AuxExportKind::Setter { class, field } => { - let arg_ty = ts_arg_tys[0].clone(); + let arg_ty = match export.generate_typescript { + true => Some(ts_arg_tys[0].as_str()), + false => None, + }; let exported = require_class(&mut self.exported_classes, class); - exported.push_setter(&docs, field, &code, &arg_ty, might_be_optional_field); + exported.push_setter(&docs, field, &code, arg_ty, might_be_optional_field); } AuxExportKind::StaticFunction { class, name } => { let exported = require_class(&mut self.exported_classes, class); - exported.push(&docs, name, "static ", &code, &ts_sig); + exported.push(&docs, name, "static ", &code, ts_sig); } AuxExportKind::Method { class, name, .. } => { let exported = require_class(&mut self.exported_classes, class); - exported.push(&docs, name, "", &code, &ts_sig); + exported.push(&docs, name, "", &code, ts_sig); } } } @@ -2865,19 +2881,27 @@ impl<'a> Context<'a> { } fn generate_enum(&mut self, enum_: &AuxEnum) -> Result<(), Error> { + let docs = format_doc_comments(&enum_.comments, None); let mut variants = String::new(); - self.typescript - .push_str(&format!("export enum {} {{", enum_.name)); + if enum_.generate_typescript { + self.typescript.push_str(&docs); + self.typescript + .push_str(&format!("export enum {} {{", enum_.name)); + } for (name, value) in enum_.variants.iter() { variants.push_str(&format!("{}:{},", name, value)); - self.typescript.push_str(&format!("\n {},", name)); + if enum_.generate_typescript { + self.typescript.push_str(&format!("\n {},", name)); + } + } + if enum_.generate_typescript { + self.typescript.push_str("\n}\n"); } - self.typescript.push_str("\n}\n"); self.export( &enum_.name, &format!("Object.freeze({{ {} }})", variants), - Some(format_doc_comments(&enum_.comments, None)), + Some(&docs), )?; Ok(()) @@ -3163,24 +3187,36 @@ fn require_class<'a>( } impl ExportedClass { - fn push(&mut self, docs: &str, function_name: &str, function_prefix: &str, js: &str, ts: &str) { + fn push( + &mut self, + docs: &str, + function_name: &str, + function_prefix: &str, + js: &str, + ts: Option<&str>, + ) { self.contents.push_str(docs); self.contents.push_str(function_prefix); self.contents.push_str(function_name); self.contents.push_str(js); self.contents.push_str("\n"); - self.typescript.push_str(docs); - self.typescript.push_str(" "); - self.typescript.push_str(function_prefix); - self.typescript.push_str(function_name); - self.typescript.push_str(ts); - self.typescript.push_str(";\n"); + if let Some(ts) = ts { + self.typescript.push_str(docs); + self.typescript.push_str(" "); + self.typescript.push_str(function_prefix); + self.typescript.push_str(function_name); + self.typescript.push_str(ts); + self.typescript.push_str(";\n"); + } } /// Used for adding a getter to a class, mainly to ensure that TypeScript /// generation is handled specially. - fn push_getter(&mut self, docs: &str, field: &str, js: &str, ret_ty: &str) { - self.push_accessor(docs, field, js, "get ", ret_ty); + fn push_getter(&mut self, docs: &str, field: &str, js: &str, ret_ty: Option<&str>) { + self.push_accessor(docs, field, js, "get "); + if let Some(ret_ty) = ret_ty { + self.push_accessor_ts(field, ret_ty); + } self.readable_properties.push(field.to_string()); } @@ -3191,28 +3227,18 @@ impl ExportedClass { docs: &str, field: &str, js: &str, - ret_ty: &str, + ret_ty: Option<&str>, might_be_optional_field: bool, ) { - let (has_setter, is_optional) = self.push_accessor(docs, field, js, "set ", ret_ty); - *has_setter = true; - *is_optional = might_be_optional_field; + self.push_accessor(docs, field, js, "set "); + if let Some(ret_ty) = ret_ty { + let (has_setter, is_optional) = self.push_accessor_ts(field, ret_ty); + *has_setter = true; + *is_optional = might_be_optional_field; + } } - fn push_accessor( - &mut self, - docs: &str, - field: &str, - js: &str, - prefix: &str, - ret_ty: &str, - ) -> (&mut bool, &mut bool) { - self.contents.push_str(docs); - self.contents.push_str(prefix); - self.contents.push_str(field); - self.contents.push_str(js); - self.contents.push_str("\n"); - + fn push_accessor_ts(&mut self, field: &str, ret_ty: &str) -> (&mut bool, &mut bool) { let (ty, has_setter, is_optional) = self .typescript_fields .entry(field.to_string()) @@ -3221,6 +3247,14 @@ impl ExportedClass { *ty = ret_ty.to_string(); (has_setter, is_optional) } + + fn push_accessor(&mut self, docs: &str, field: &str, js: &str, prefix: &str) { + self.contents.push_str(docs); + self.contents.push_str(prefix); + self.contents.push_str(field); + self.contents.push_str(js); + self.contents.push_str("\n"); + } } #[test] diff --git a/crates/cli-support/src/wit/mod.rs b/crates/cli-support/src/wit/mod.rs index 2c7e57d6..878581dc 100644 --- a/crates/cli-support/src/wit/mod.rs +++ b/crates/cli-support/src/wit/mod.rs @@ -454,6 +454,7 @@ impl<'a> Context<'a> { comments: concatenate_comments(&export.comments), arg_names: Some(export.function.arg_names), kind, + generate_typescript: export.function.generate_typescript, }, ); Ok(()) @@ -767,6 +768,7 @@ impl<'a> Context<'a> { .iter() .map(|v| (v.name.to_string(), v.value)) .collect(), + generate_typescript: enum_.generate_typescript, }; self.aux.enums.push(aux); Ok(()) @@ -799,6 +801,7 @@ impl<'a> Context<'a> { class: struct_.name.to_string(), field: field.name.to_string(), }, + generate_typescript: field.generate_typescript, }, ); @@ -824,6 +827,7 @@ impl<'a> Context<'a> { class: struct_.name.to_string(), field: field.name.to_string(), }, + generate_typescript: field.generate_typescript, }, ); } @@ -831,6 +835,7 @@ impl<'a> Context<'a> { name: struct_.name.to_string(), comments: concatenate_comments(&struct_.comments), is_inspectable: struct_.is_inspectable, + generate_typescript: struct_.generate_typescript, }; self.aux.structs.push(aux); @@ -1048,6 +1053,7 @@ impl<'a> Context<'a> { comments: String::new(), arg_names: None, kind, + generate_typescript: true, }; assert!(self.aux.export_map.insert(id, export).is_none()); } diff --git a/crates/cli-support/src/wit/nonstandard.rs b/crates/cli-support/src/wit/nonstandard.rs index 0687e7a9..be65fc4d 100644 --- a/crates/cli-support/src/wit/nonstandard.rs +++ b/crates/cli-support/src/wit/nonstandard.rs @@ -73,6 +73,8 @@ pub struct AuxExport { pub arg_names: Option>, /// What kind of function this is and where it shows up pub kind: AuxExportKind, + /// Whether typescript bindings should be generated for this export. + pub generate_typescript: bool, } /// All possible kinds of exports from a wasm module. @@ -131,7 +133,10 @@ pub struct AuxEnum { /// The copied Rust comments to forward to JS pub comments: String, /// A list of variants with their name and value + /// and whether typescript bindings should be generated for each variant pub variants: Vec<(String, u32)>, + /// Whether typescript bindings should be generated for this enum. + pub generate_typescript: bool, } #[derive(Debug)] @@ -142,6 +147,8 @@ pub struct AuxStruct { pub comments: String, /// Whether to generate helper methods for inspecting the class pub is_inspectable: bool, + /// Whether typescript bindings should be generated for this struct. + pub generate_typescript: bool, } /// All possible types of imports that can be imported by a wasm module. diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index a2bcdc3a..3a15081f 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -51,6 +51,7 @@ macro_rules! attrgen { (vendor_prefix, VendorPrefix(Span, Ident)), (variadic, Variadic(Span)), (typescript_custom_section, TypescriptCustomSection(Span)), + (skip_typescript, SkipTypescript(Span)), (start, Start(Span)), (skip, Skip(Span)), (typescript_type, TypeScriptType(Span, String, Span)), @@ -354,9 +355,11 @@ impl<'a> ConvertToAst for &'a mut syn::ItemStruct { getter: Ident::new(&getter, Span::call_site()), setter: Ident::new(&setter, Span::call_site()), comments, + generate_typescript: attrs.skip_typescript().is_none(), }); attrs.check_used()?; } + let generate_typescript = attrs.skip_typescript().is_none(); let comments: Vec = extract_doc_comments(&self.attrs); attrs.check_used()?; Ok(ast::Struct { @@ -365,6 +368,7 @@ impl<'a> ConvertToAst for &'a mut syn::ItemStruct { fields, comments, is_inspectable, + generate_typescript, }) } } @@ -720,6 +724,7 @@ fn function_from_decl( rust_attrs: attrs, rust_vis: vis, r#async: sig.asyncness.is_some(), + generate_typescript: opts.skip_typescript().is_none(), }, method_self, )) @@ -798,11 +803,12 @@ impl<'a> MacroParse<(Option, &'a mut TokenStream)> for syn::Item { }; f.macro_parse(program, opts)?; } - syn::Item::Enum(e) => { - if let Some(opts) = opts { - opts.check_used()?; - } - e.macro_parse(program, (tokens,))?; + syn::Item::Enum(mut e) => { + let opts = match opts { + Some(opts) => opts, + None => BindgenAttrs::find(&mut e.attrs)?, + }; + e.macro_parse(program, (tokens, opts))?; } syn::Item::Const(mut c) => { let opts = match opts { @@ -1032,15 +1038,17 @@ fn import_enum(enum_: syn::ItemEnum, program: &mut ast::Program) -> Result<(), D Ok(()) } -impl<'a> MacroParse<(&'a mut TokenStream,)> for syn::ItemEnum { +impl<'a> MacroParse<(&'a mut TokenStream, BindgenAttrs)> for syn::ItemEnum { fn macro_parse( self, program: &mut ast::Program, - (tokens,): (&'a mut TokenStream,), + (tokens, opts): (&'a mut TokenStream, BindgenAttrs), ) -> Result<(), Diagnostic> { if self.variants.len() == 0 { bail_span!(self, "cannot export empty enums to JS"); } + let generate_typescript = opts.skip_typescript().is_none(); + opts.check_used()?; // Check if the first value is a string literal match self.variants[0].discriminant { @@ -1141,8 +1149,8 @@ impl<'a> MacroParse<(&'a mut TokenStream,)> for syn::ItemEnum { variants, comments, hole, + generate_typescript, }); - Ok(()) } } diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index ceea8baf..abc1901d 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -100,6 +100,7 @@ macro_rules! shared_api { name: &'a str, variants: Vec>, comments: Vec<&'a str>, + generate_typescript: bool, } struct EnumVariant<'a> { @@ -110,6 +111,7 @@ macro_rules! shared_api { struct Function<'a> { arg_names: Vec, name: &'a str, + generate_typescript: bool, } struct Struct<'a> { @@ -117,12 +119,14 @@ macro_rules! shared_api { fields: Vec>, comments: Vec<&'a str>, is_inspectable: bool, + generate_typescript: bool, } struct StructField<'a> { name: &'a str, readonly: bool, comments: Vec<&'a str>, + generate_typescript: bool, } struct LocalModule<'a> { diff --git a/crates/typescript-tests/Cargo.toml b/crates/typescript-tests/Cargo.toml index bf617379..61efd4c1 100644 --- a/crates/typescript-tests/Cargo.toml +++ b/crates/typescript-tests/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" [dependencies] wasm-bindgen = { path = '../..' } web-sys = { path = '../web-sys', features = [ 'HtmlElement', 'Node', 'Document' ] } +js-sys = { path = '../js-sys' } [lib] crate-type = ['cdylib'] diff --git a/crates/typescript-tests/src/lib.rs b/crates/typescript-tests/src/lib.rs index 6b2fa26c..5336adf4 100644 --- a/crates/typescript-tests/src/lib.rs +++ b/crates/typescript-tests/src/lib.rs @@ -1,5 +1,6 @@ pub mod custom_section; pub mod getters_setters; +pub mod omit_definition; pub mod opt_args_and_ret; pub mod optional_fields; pub mod simple_fn; diff --git a/crates/typescript-tests/src/omit_definition.rs b/crates/typescript-tests/src/omit_definition.rs new file mode 100644 index 00000000..01fa45e0 --- /dev/null +++ b/crates/typescript-tests/src/omit_definition.rs @@ -0,0 +1,34 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TYPE_GET_VALUE: &'static str = + "export function take_function(func: (x: number) => void): void;"; + +#[wasm_bindgen(skip_typescript)] +pub fn take_function(_: js_sys::Function) {} + +#[wasm_bindgen] +pub struct MyExportedStruct { + #[wasm_bindgen(skip_typescript)] + pub field: bool, +} + +#[wasm_bindgen] +impl MyExportedStruct { + #[wasm_bindgen(skip_typescript)] + pub fn method(&mut self) { + self.field = !self.field; + } + + #[wasm_bindgen(skip_typescript)] + pub fn static_func() { + panic!("oh no!"); + } +} + +#[wasm_bindgen(skip_typescript)] +pub enum MyEnum { + One, + Two, + Three, +} diff --git a/crates/typescript-tests/src/omit_definition.ts b/crates/typescript-tests/src/omit_definition.ts new file mode 100644 index 00000000..04e4fcca --- /dev/null +++ b/crates/typescript-tests/src/omit_definition.ts @@ -0,0 +1,26 @@ +import * as wbg from '../pkg/typescript_tests'; + +wbg.take_function((value) => { + // `value` should be inferred as `number` because of the + // custom typescript section. If `typescript = false` does + // not prevent the generation of a signature that takes any + // function, then this will trigger a noImplicitAny error. + console.log(value); +}); + +declare function assert(message: T): void; + +type EnableIfEnum = "MyEnum" extends keyof typeof wbg ? never : string; +assert("`MyEnum` type should not be exported."); + +type EnableIfStruct = "MyStruct" extends keyof typeof wbg ? never : string; +assert("`MyStruct` type should not be exported."); + +type EnableIfField = "field" extends keyof wbg.MyExportedStruct ? never : string; +assert("`field` should not exist on `MyExportedStruct`."); + +type EnableIfMethod = "method" extends keyof wbg.MyExportedStruct ? never : string; +assert("`method` should not exist on `MyExportedStruct`."); + +type EnableIfStaticMethod = "static_func" extends keyof typeof wbg.MyExportedStruct ? never : string; +assert("`static_func` should not exist on `MyExportedStruct`."); diff --git a/guide/src/reference/attributes/on-rust-exports/skip_typescript.md b/guide/src/reference/attributes/on-rust-exports/skip_typescript.md new file mode 100644 index 00000000..100e9a4e --- /dev/null +++ b/guide/src/reference/attributes/on-rust-exports/skip_typescript.md @@ -0,0 +1,42 @@ +# `skip_typescript` + +By default, Rust exports exposed to JavaScript will generate TypeScript definitions (unless `--no-typescript` is used). The `skip_typescript` attribute can be used to disable type generation per function, enum, struct, or field. For example: + +```rust +#[wasm_bindgen(skip_typescript)] +pub enum MyHiddenEnum { + One, + Two, + Three +} + +#[wasm_bindgen] +pub struct MyPoint { + pub x: u32, + + #[wasm_bindgen(skip_typescript)] + pub y: u32, +} + +#[wasm_bindgen] +impl MyPoint { + + #[wasm_bindgen(skip_typescript)] + pub fn stringify(&self) -> String { + format!("({}, {})", self.x, self.y) + } +} +``` + +Will generate the following `.d.ts` file: + +```ts +/* tslint:disable */ +/* eslint-disable */ +export class MyPoint { + free(): void; + x: number; +} +``` + +When combined with [the `typescript_custom_section` attribute](typescript_custom_section.html), this can be used to manually specify more specific function types instead of using the generated definitions.