From fa8961e56a1dae0ab8696c5a949dea5706cf2480 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Mon, 4 Jun 2018 16:44:47 -0300 Subject: [PATCH] Add prototype of wasm-bindgen-typescript --- Cargo.toml | 1 + crates/typescript/Cargo.toml | 15 +++ crates/typescript/README.md | 10 ++ crates/typescript/api-extractor.json | 10 ++ crates/typescript/package.json | 20 ++++ crates/typescript/src/api_extractor.rs | 16 +++ crates/typescript/src/definitions.rs | 81 +++++++++++++++ crates/typescript/src/lib.rs | 10 ++ crates/typescript/src/main.rs | 16 +++ crates/typescript/src/parser.rs | 138 +++++++++++++++++++++++++ crates/typescript/tsconfig.json | 15 +++ 11 files changed, 332 insertions(+) create mode 100644 crates/typescript/Cargo.toml create mode 100644 crates/typescript/README.md create mode 100644 crates/typescript/api-extractor.json create mode 100644 crates/typescript/package.json create mode 100644 crates/typescript/src/api_extractor.rs create mode 100644 crates/typescript/src/definitions.rs create mode 100644 crates/typescript/src/lib.rs create mode 100644 crates/typescript/src/main.rs create mode 100644 crates/typescript/src/parser.rs create mode 100644 crates/typescript/tsconfig.json diff --git a/Cargo.toml b/Cargo.toml index 63dfee7e..6d9abcc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ wasm-bindgen-cli-support = { path = "crates/cli-support", version = '=0.2.11' } [workspace] members = [ "crates/cli", + "crates/typescript", "crates/webidl", "examples/hello_world", "examples/smorgasboard", diff --git a/crates/typescript/Cargo.toml b/crates/typescript/Cargo.toml new file mode 100644 index 00000000..b9949440 --- /dev/null +++ b/crates/typescript/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "wasm-bindgen-typescript" +version = "0.1.0" +authors = ["Santiago Pastorino "] + +[dependencies] +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" + +proc-macro2 = "0.4" +quote = "0.6" +syn = { version = "0.14", default-features = false } +wasm-bindgen = { path = "../..", default-features = false } +wasm-bindgen-backend = { path = "../backend", default-features = false } diff --git a/crates/typescript/README.md b/crates/typescript/README.md new file mode 100644 index 00000000..c75a400d --- /dev/null +++ b/crates/typescript/README.md @@ -0,0 +1,10 @@ +# TypeScript file + +Copy your TypeScript file over the one in `ts/index.d.ts` + +# Run + +``` +$ npm install -g @microsoft/api-extractor +$ cargo run +``` diff --git a/crates/typescript/api-extractor.json b/crates/typescript/api-extractor.json new file mode 100644 index 00000000..5c3bb943 --- /dev/null +++ b/crates/typescript/api-extractor.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://dev.office.com/json-schemas/api-extractor/api-extractor.schema.json", + "compiler" : { + "configType": "tsconfig", + "rootFolder": "." + }, + "project": { + "entryPointSourceFile": "ts/index.d.ts" + } +} diff --git a/crates/typescript/package.json b/crates/typescript/package.json new file mode 100644 index 00000000..194ea04b --- /dev/null +++ b/crates/typescript/package.json @@ -0,0 +1,20 @@ +{ + "name": "wasm", + "version": "1.0.0", + "description": "", + "main": "index.js", + "directories": { + "lib": "lib", + "test": "test" + }, + "dependencies": { + "@microsoft/api-extractor": "^5.6.3" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/crates/typescript/src/api_extractor.rs b/crates/typescript/src/api_extractor.rs new file mode 100644 index 00000000..0e1711da --- /dev/null +++ b/crates/typescript/src/api_extractor.rs @@ -0,0 +1,16 @@ +use std::process::Command; + +pub(crate) fn run() { + let output = Command::new("api-extractor") + .arg("run") + .output() + .expect("api-extractor not installed?"); + + let out = if output.status.success() { + output.stdout + } else { + output.stderr + }; + + print!("{}", String::from_utf8_lossy(out.as_slice())); +} diff --git a/crates/typescript/src/definitions.rs b/crates/typescript/src/definitions.rs new file mode 100644 index 00000000..11a55694 --- /dev/null +++ b/crates/typescript/src/definitions.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; + +// Public API types for a TypeScript project based on +// https://github.com/Microsoft/web-build-tools/blob/master/apps/api-extractor/src/api/api-json.schema.json +// +// There are some attributes that are omitted because they are not relevant to +// us. +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct TsPackage { + kind: String, + name: String, + pub(crate) exports: HashMap, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "kind")] +pub(crate) enum TsExport { + #[serde(rename = "class")] + TsClass { + members: HashMap, + }, + + #[serde(rename = "function")] + TsFunction { + parameters: HashMap, + #[serde(rename = "returnValue")] + return_value: TsReturnValue, + }, + + //TODO: implement ... + //{ "$ref": "#/definitions/interfaceApiItem" }, + //{ "$ref": "#/definitions/namespaceApiItem" }, + //{ "$ref": "#/definitions/enumApiItem" }, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "kind")] +pub(crate) enum TsClassMembers { + #[serde(rename = "property")] + TsProperty { + #[serde(rename = "isStatic")] + is_static: bool, + #[serde(rename = "isReadOnly")] + is_read_only: bool, + #[serde(rename = "type")] + property_type: String, + }, + + #[serde(rename = "constructor")] + TsConstructor { + parameters: HashMap, + }, + + #[serde(rename = "method")] + TsMethod { + #[serde(rename = "accessModifier")] + access_modifier: String, + #[serde(rename = "isStatic")] + is_static: bool, + parameters: HashMap, + #[serde(rename = "returnValue")] + return_value: TsReturnValue, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct TsMethodProperty { + name: String, + #[serde(rename = "type")] + pub(crate) property_type: String, + #[serde(rename = "isSpread")] + is_spread: bool, + #[serde(rename = "isOptional")] + is_optional: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct TsReturnValue { + #[serde(rename = "type")] + pub(crate) property_type: String, +} diff --git a/crates/typescript/src/lib.rs b/crates/typescript/src/lib.rs new file mode 100644 index 00000000..10f9c15b --- /dev/null +++ b/crates/typescript/src/lib.rs @@ -0,0 +1,10 @@ +extern crate proc_macro2; +extern crate serde; +#[macro_use] extern crate serde_derive; +extern crate serde_json; +extern crate syn; +extern crate wasm_bindgen_backend as backend; + +pub mod api_extractor; +pub mod definitions; +pub mod parser; diff --git a/crates/typescript/src/main.rs b/crates/typescript/src/main.rs new file mode 100644 index 00000000..012cf5bc --- /dev/null +++ b/crates/typescript/src/main.rs @@ -0,0 +1,16 @@ +extern crate proc_macro2; +extern crate quote; +extern crate wasm_bindgen_typescript; + +use wasm_bindgen_typescript::parser; + +use proc_macro2::TokenStream; +use quote::ToTokens; + +fn main() { + let program = parser::ts_to_program("dist/wasm.api.json"); + + let mut tokens = TokenStream::new(); + program.to_tokens(&mut tokens); + println!("{}", tokens); +} diff --git a/crates/typescript/src/parser.rs b/crates/typescript/src/parser.rs new file mode 100644 index 00000000..1fb8e9fc --- /dev/null +++ b/crates/typescript/src/parser.rs @@ -0,0 +1,138 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; + +use backend::{self}; +use backend::ast::{BindgenAttrs, Export, Function}; +use proc_macro2::{self}; +use serde_json::{self}; +use syn::{self}; + +use api_extractor; +use definitions::*; + +pub fn ts_to_program(file_name: &str) -> backend::ast::Program { + api_extractor::run(); + + let ts_package = parse_json(file_name); + + let mut program = backend::ast::Program::default(); + + for (name, export) in ts_package.exports { + match export { + TsExport::TsClass { members } => { + for (member_name, member) in members { + match member { + TsClassMembers::TsProperty { .. } => {} + TsClassMembers::TsConstructor { .. } => {} + TsClassMembers::TsMethod { parameters, return_value, .. } => { + let function = build_function(member_name, parameters, return_value); + + program.exports.push(Export { + class: Some(syn::Ident::new(&name, proc_macro2::Span::call_site())), + method: true, + mutable: false, + constructor: None, + function, + }); + } + } + } + } + + TsExport::TsFunction { parameters, return_value } => { + let function = build_function(name, parameters, return_value); + + program.exports.push(Export { + class: None, + method: false, + mutable: false, + constructor: None, + function, + }); + } + } + } + + program +} + +fn parse_json(file_name: &str) -> TsPackage { + let mut file = File::open(file_name).unwrap(); + let mut data = String::new(); + file.read_to_string(&mut data).unwrap(); + + serde_json::from_str(&data).unwrap() +} + +fn build_function(name: String, parameters: HashMap, return_value: TsReturnValue) -> Function { + let arguments = parameters.iter().map( |(_name, property)| { + let property_type = rust_type(&property.property_type); + + let mut segments = syn::punctuated::Punctuated::new(); + segments.push(syn::PathSegment { + ident: syn::Ident::new(property_type, proc_macro2::Span::call_site()), + arguments: syn::PathArguments::None, + }); + + syn::Type::Path(syn::TypePath { + qself: None, + path: syn::Path { + leading_colon: None, + segments, + } + }) + }).collect::>(); + + let ret_property_type = rust_type(&return_value.property_type); + + let mut ret_segments = syn::punctuated::Punctuated::new(); + ret_segments.push(syn::PathSegment { + ident: syn::Ident::new(ret_property_type, proc_macro2::Span::call_site()), + arguments: syn::PathArguments::None, + }); + + let ret = syn::Type::Path(syn::TypePath { + qself: None, + path: syn::Path { + leading_colon: None, + segments: ret_segments, + } + }); + + let rust_decl = Box::new(syn::FnDecl { + fn_token: Default::default(), + generics: Default::default(), + paren_token: Default::default(), + //TODO investigate if inputs should be taken from arguments + inputs: Default::default(), + variadic: None, + output: syn::ReturnType::Type(Default::default(), Box::new(ret.clone())), + }); + + Function { + name: syn::Ident::new(&name, proc_macro2::Span::call_site()), + arguments, + ret: Some(ret), + opts: BindgenAttrs::default(), + rust_attrs: Vec::new(), + rust_decl, + rust_vis: syn::Visibility::Public(syn::VisPublic { + pub_token: Default::default(), + }), + } +} + +// TODO: implement this properly +fn rust_type(js_type: &str) -> &'static str { + match js_type { + "string" => "String", + "number" => "String", + "boolean" => "bool", + "symbol" => "String", + "object" => "String", + "function" => "String", + "void" => "String", + _ => "String", + } +} diff --git a/crates/typescript/tsconfig.json b/crates/typescript/tsconfig.json new file mode 100644 index 00000000..c0737c79 --- /dev/null +++ b/crates/typescript/tsconfig.json @@ -0,0 +1,15 @@ +{ + "version": "2.4.2", + "compilerOptions": { + "lib": ["es5", "es6"], + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true + }, + "exclude": [ + "node_modules" + ] +}