Skip to content

Add prototype of wasm-bindgen-typescript #228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions crates/typescript/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "wasm-bindgen-typescript"
version = "0.1.0"
authors = ["Santiago Pastorino <[email protected]>"]

[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 }
10 changes: 10 additions & 0 deletions crates/typescript/README.md
Original file line number Diff line number Diff line change
@@ -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
```
10 changes: 10 additions & 0 deletions crates/typescript/api-extractor.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
20 changes: 20 additions & 0 deletions crates/typescript/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
16 changes: 16 additions & 0 deletions crates/typescript/src/api_extractor.rs
Original file line number Diff line number Diff line change
@@ -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()));
}
81 changes: 81 additions & 0 deletions crates/typescript/src/definitions.rs
Original file line number Diff line number Diff line change
@@ -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<String, TsExport>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "kind")]
pub(crate) enum TsExport {
#[serde(rename = "class")]
TsClass {
members: HashMap<String, TsClassMembers>,
},

#[serde(rename = "function")]
TsFunction {
parameters: HashMap<String, TsMethodProperty>,
#[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<String, TsMethodProperty>,
},

#[serde(rename = "method")]
TsMethod {
#[serde(rename = "accessModifier")]
access_modifier: String,
#[serde(rename = "isStatic")]
is_static: bool,
parameters: HashMap<String, TsMethodProperty>,
#[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,
}
10 changes: 10 additions & 0 deletions crates/typescript/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 16 additions & 0 deletions crates/typescript/src/main.rs
Original file line number Diff line number Diff line change
@@ -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);
}
138 changes: 138 additions & 0 deletions crates/typescript/src/parser.rs
Original file line number Diff line number Diff line change
@@ -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<String, TsMethodProperty>, 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::<Vec<_>>();

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",
}
}
15 changes: 15 additions & 0 deletions crates/typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}