Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
remojansen committed Nov 14, 2016
1 parent ad7900a commit a9c10b7
Show file tree
Hide file tree
Showing 15 changed files with 492 additions and 37 deletions.
37 changes: 1 addition & 36 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,37 +1,2 @@
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
out
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history
65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,64 @@
# MVPSummit2016Hackathon
# 2016 Global MVP Summit Hackathon
This VS Code extension was developed during a hackathon at the 2016 Global MVP Summit.

This extension allows you to auto-generate UML diagrams for TypeScript applications:

![](preview.gif)

## About this repo
The application was finished during my flight back from Seattle to Ireland. Please do not expect this to be a production-ready extension: **IT IS JUST AN EXPERIMENT**.

I decided to share the repo because it could be used as a reference in the development
of VS Code extensions.

You can use this repo to find out the following:

- How to use the TypeScript language service to access the AST generagted by the TypeScript compiler.
- How to traverse the AST.
- How to access user-geerated source code from a VS Code extension.
- How to create custom VS Code commands.
- How to perform web request from a VS Code extension.
- How to render a new page after triggering custom VS Code commands.

## About the source code
This repo is divided in two main components.

### /src/core
The `core` folder exposes a function named `getDiagram`.

This function expects an array of paths to be passed as
an argument. The paths should point to some TypeScript
source files.

The `getDiagram` function uses other core components:

- **Parser**: Traversed the TypeScript AST to create a `ClassDetails` data structure.
- **Serializer**: Transfroms `ClassDetails` data structure into the UML DSL.
- **Renderer**: Transforms the DSL into a SVG class diagram.

The function returns a `Promise<string>`. If the promise
is fulfilled, the returned string value will contain a svg diagram.

### /src/extension
Contains the actial VS Code extension. It declares a new custom
command. When the command is executed the current source file is
passed to the code to get a class diagram. The diagram is then
displayed in a new panel.

### /test/
TODO

### /test/data
TODO

## Missing features
Feel free to send PRs:
- Display inheritance relationships
- Display composition relationships
- Display "implements"

## Resources
I used the following links during the hackathon:
- https://code.visualstudio.com/docs/extensionAPI/overview
- https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
- http://www.nomnoml.com/
38 changes: 38 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "tsuml",
"displayName": "TsUML",
"description": "Generate class diagrams for TypeScript",
"version": "0.0.1",
"publisher": "owerreloaded",
"engines": {
"vscode": "^1.5.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCommand:extension.tsuml"
],
"main": "./out/src/extension",
"contributes": {
"commands": [{
"command": "extension.tsuml",
"title": "Show Class Diagram"
}]
},
"scripts": {
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"dependencies": {
"nomnoml": "0.0.4"
},
"devDependencies": {
"typescript": "^2.0.3",
"vscode": "^1.0.0",
"mocha": "^2.3.3",
"@types/node": "^6.0.40",
"@types/mocha": "^2.2.32"
}
}
Binary file added preview.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions src/core/dts.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
declare module "nomnoml" {

interface nomnoml {
renderSvg(umlString: string): string;
}

module nomnoml {}

var nomnoml: nomnoml;
export = nomnoml;

}
28 changes: 28 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as ts from "typescript";
import parse from "./parser/parser";
import serialize from "./serializer/serializer";
import render from "./renderer/renderer";

function getDiagram(tsFilePaths: string[]): Promise<string> {
return new Promise<string>((resolve, reject) => {

if (tsFilePaths.length === 0) {
reject("Missing input files!");
}

let options = {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS
};

let classesDetails = parse(tsFilePaths, options);
let umlStr = classesDetails.map((classDetails) => serialize(classDetails)).reduce((p, c) => `${p}\n${c}`, "");

render(umlStr).then((svg) => {
resolve(svg);
});

});
}

export default getDiagram;
28 changes: 28 additions & 0 deletions src/core/interfaces/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module interfaces {

export interface PropDetails {
name: string;
type: string;
}

export interface ArgDetails {
name: string;
type: string;
}

export interface MethodDetails {
name: string;
returnType: string;
args: ArgDetails[];
}

export interface ClassDetails {
name: string;
props: PropDetails[];
methods: MethodDetails[];
}

}

export default interfaces;

119 changes: 119 additions & 0 deletions src/core/parser/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as ts from "typescript";
import interfaces from "../interfaces/interfaces";

function parse(fileNames: string[], options: ts.CompilerOptions): interfaces.ClassDetails[] {

// Build a program using the set of root file names in fileNames
let program = ts.createProgram(fileNames, options);

// Get the checker, we will use it to find more about classes
let checker = program.getTypeChecker();

// The final result
let output: interfaces.ClassDetails[] = [];

// visit nodes finding exported classes
function visit(node: ts.Node) {

// Only consider exported nodes
if (!isNodeExported(node)) {
return;
}

if (node.kind === ts.SyntaxKind.ClassDeclaration) {
// This is a top level class, get its symbol
let symbol = checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);
output.push(serializeClass(symbol));
}
else if (node.kind === ts.SyntaxKind.ModuleDeclaration) {
// This is a namespace, visit its children
ts.forEachChild(node, visit);
}

}

// Serialize a class symbol infomration
function serializeClass(symbol: ts.Symbol) {

let classDetails: interfaces.ClassDetails = {
name: symbol.getName(),
props: [],
methods: []
};

classDetails.props = serializeProperties(symbol);
classDetails.methods = serializeMethods(symbol);
return classDetails;

}

function serializeProperties(symbol: ts.Symbol): interfaces.PropDetails[] {

let props: interfaces.PropDetails[] = [];

ts.forEachChild(symbol.valueDeclaration, (node) => {

if (node.kind === ts.SyntaxKind.PropertyDeclaration) {

let symbol = checker.getSymbolAtLocation((<ts.PropertyDeclaration>node).name);
let propertyType = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);

props.push({
name: symbol.name,
type: checker.typeToString(propertyType)
});

}

});

return props;

}

function serializeMethods(symbol: ts.Symbol): interfaces.MethodDetails[] {

let methods: interfaces.MethodDetails[] = [];

ts.forEachChild(symbol.valueDeclaration, (node) => {

if (node.kind === ts.SyntaxKind.MethodDeclaration) {

let symbol = checker.getSymbolAtLocation((<ts.MethodDeclaration>node).name);
let methodType = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
let methodSignature = methodType.getCallSignatures()[0];

methods.push({
name: symbol.name,
returnType: checker.typeToString(methodSignature.getReturnType()),
args: methodSignature.getParameters().map((parameter) => {
let parameterType = checker.getTypeOfSymbolAtLocation(parameter, parameter.valueDeclaration);
return {
name: parameter.getName(),
type: checker.typeToString(parameterType)
};
})
});
}

});

return methods;

}

// True if this is visible outside this file, false otherwise
function isNodeExported(node: ts.Node): boolean {
return (node.flags & ts.NodeFlags.Export) !== 0 || (node.parent && node.parent.kind === ts.SyntaxKind.SourceFile);
}

// Visit every sourceFile in the program
for (const sourceFile of program.getSourceFiles()) {
// Walk the tree to search for classes
ts.forEachChild(sourceFile, visit);
}

return output;
}

export default parse;
17 changes: 17 additions & 0 deletions src/core/renderer/renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import interfaces from "../interfaces/interfaces";
import * as http from "http";
import * as fs from "fs";
import * as nomnoml from "nomnoml";

function render(umlString: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
let svg = nomnoml.renderSvg(umlString);
resolve(svg);
} catch(e) {
reject(e);
}
});
}

export default render;
Loading

0 comments on commit a9c10b7

Please sign in to comment.