Repository: remojansen/TsUML
Branch: master
Commit: 44694fe847ce
Files: 19
Total size: 12.5 KB
Directory structure:
gitextract_uoq8f55i/
├── .gitignore
├── .npmignore
├── .vscodeignore
├── LICENSE
├── README.md
├── package.json
├── src/
│ ├── bin/
│ │ └── index.ts
│ ├── core/
│ │ ├── emitter.ts
│ │ ├── index.ts
│ │ ├── interfaces.ts
│ │ ├── io.ts
│ │ ├── parser.ts
│ │ └── templates.ts
│ ├── demo/
│ │ ├── interfaces.ts
│ │ ├── katana.ts
│ │ ├── main.ts
│ │ └── ninja.ts
│ └── lib/
│ └── index.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
out
node_modules
dist
================================================
FILE: .npmignore
================================================
src
tsconfig.json
assets
.vscodeignore
================================================
FILE: .vscodeignore
================================================
.vscode/**
.vscode-test/**
out/test/**
test/**
src/**
**/*.map
.gitignore
tsconfig.json
vsc-extension-quickstart.md
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2016-2018 Remo H. Jansen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# TsUML
:construction: WORK IN PROGRESS :construction:
Generate UML diagram for your TypeScript applications powered by https://yuml.me/

## Installation
```sh
npm install -g tsuml
```
## Usage
```
tsuml --glob ./src/**/*.ts
```
The diagram generated for the code under the [demo folder](https://github.com/remojansen/TsUML/tree/master/src/demo) looks as follows:

================================================
FILE: package.json
================================================
{
"name": "tsuml",
"version": "0.0.1-alpha.8",
"description": "UML diagrams for TypeScript",
"main": "dist/lib/index.js",
"bin": {
"tsuml": "dist/bin/index.js"
},
"devDependencies": {
"@types/glob": "5.0.35",
"@types/lodash": "4.14.106",
"@types/node": "9.6.0",
"@types/opn": "5.1.0",
"@types/request": "2.47.0",
"@types/yargs": "11.0.0"
},
"dependencies": {
"chalk": "2.3.2",
"glob": "7.1.2",
"lodash": "4.17.5",
"opn": "5.3.0",
"request": "2.85.0",
"ts-simple-ast": "9.5.0",
"typescript": "2.7.2",
"yargs": "11.0.0"
},
"scripts": {
"test": "tsc -p tsconfig.json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/remojansen/TsUML.git"
},
"keywords": [
"typescript",
"uml",
"diagram",
"generator",
"class"
],
"author": "Remo H. Jansen",
"license": "MIT",
"bugs": {
"url": "https://github.com/remojansen/TsUML/issues"
},
"homepage": "https://github.com/remojansen/TsUML#readme"
}
================================================
FILE: src/bin/index.ts
================================================
#! /usr/bin/env node
import chalk from "chalk";
import * as yargs from "yargs";
import { getUrl } from "../core";
(async () => {
try {
if (yargs.argv.help) {
console.log(chalk.yellowBright("tsuml --glob ./src/**/*.ts"));
}
const pattern = yargs.argv.glob;
if (!pattern) {
console.log(chalk.redBright("Missing --glob"));
} else {
const url = await getUrl("./tsconfig.json", pattern);
const opn = require("opn");
opn(url);
}
} catch(e) {
console.log(chalk.redBright(e));
}
})();
================================================
FILE: src/core/emitter.ts
================================================
import Ast, * as SimpleAST from "ts-simple-ast";
import * as ts from "typescript";
import { flatten, join } from "lodash";
import * as path from "path";
import { PropertyDetails, MethodDetails, HeritageClause } from "./interfaces";
import { templates }from "./templates";
import { download } from "./io";
export function emitSingleClass(name: string, properties: PropertyDetails[], methods: MethodDetails[]) {
return templates.class(name, properties, methods);
}
export function emitSingleInterface(name: string, properties: PropertyDetails[], methods: MethodDetails[]) {
return templates.interface(name, properties, methods);
}
export function emitHeritageClauses(heritageClauses: HeritageClause[]) {
return heritageClauses.map((heritageClause) =>
templates.implementsOrExtends(heritageClause.clause, heritageClause.className)
);
}
================================================
FILE: src/core/index.ts
================================================
import * as fs from "fs";
import chalk from "chalk";
import { flatten, join } from "lodash";
import { findFilesByGlob, download } from "./io";
import { getAst, parseClasses, parseInterfaces, parseHeritageClauses } from "./parser";
import { emitSingleClass, emitSingleInterface, emitHeritageClauses } from "./emitter";
async function getDsl(tsConfigPath: string, pattern: string) {
const sourceFilesPaths = await findFilesByGlob(pattern);
console.log(
chalk.yellowBright(
"Matched files:\n" + sourceFilesPaths.reduce((p, c) => `${p}${c}\n`, "")
)
);
const ast = getAst(tsConfigPath, sourceFilesPaths);
const files = ast.getSourceFiles();
// parser
const declarations = files.map(f => {
const classes = f.getClasses();
const interfaces = f.getInterfaces();
const path = f.getFilePath();
return {
fileName: path,
classes: classes.map(parseClasses),
heritageClauses: classes.map(parseHeritageClauses),
interfaces: interfaces.map(parseInterfaces)
};
});
// emitter
const entities = declarations.map(d => {
const classes = d.classes.map((c) => emitSingleClass(c.className, c.properties, c.methods));
const interfaces = d.interfaces.map((i) => emitSingleInterface(i.interfaceName, i.properties, i.methods));
const heritageClauses = d.heritageClauses.map(emitHeritageClauses);
return [...classes, ...interfaces, ...heritageClauses];
});
return join(flatten(entities), ",");
}
export async function getUrl(tsConfigPath: string, pattern: string) {
const dsl = await getDsl(tsConfigPath, pattern);
return await download(dsl);
}
================================================
FILE: src/core/interfaces.ts
================================================
export interface MethodDetails {
name: string;
}
export interface PropertyDetails {
name: string;
}
export interface HeritageClause {
clause: string;
className: string;
}
================================================
FILE: src/core/io.ts
================================================
import * as glob from "glob";
import * as request from "request";
import * as fs from "fs";
export async function findFilesByGlob(pattern: string) {
return new Promise((res, rej) => {
glob(pattern, (err, files) => {
if (err) {
rej(err);
} else {
res(files);
}
});
});
}
export async function download(dsl: string) {
return new Promise((resolve, reject) => {
const url = "https://yuml.me/diagram/plain/class/";
const options = {
form: {
dsl_text: dsl
}
};
request.post(url, options, (err, res, body) => {
if (err) {
reject(err);
}
const svgFileName = body.replace(".png", ".svg");
const diagramUrl = `${url}${svgFileName}`;
resolve(diagramUrl);
});
});
};
================================================
FILE: src/core/parser.ts
================================================
import Ast, * as SimpleAST from "ts-simple-ast";
import * as ts from "typescript";
import { flatten, join } from "lodash";
import { PropertyDetails, MethodDetails, HeritageClause } from "./interfaces";
export function getAst(tsConfigPath: string, sourceFilesPaths?: string[]) {
const ast = new Ast({
tsConfigFilePath: tsConfigPath,
addFilesFromTsConfig: !Array.isArray(sourceFilesPaths)
});
if (sourceFilesPaths) {
ast.addExistingSourceFiles(sourceFilesPaths);
}
return ast;
}
export function parseClasses(classDeclaration: SimpleAST.ClassDeclaration) {
const className = classDeclaration.getSymbol()!.getName();
const propertyDeclarations = classDeclaration.getProperties();
const methodDeclarations = classDeclaration.getMethods();
const properties = propertyDeclarations.map(property => {
const sym = property.getSymbol();
if (sym) {
return {
name: sym.getName()
};
}
}).filter((p) => p !== undefined) as PropertyDetails[];
const methods = methodDeclarations.map(method => {
const sym = method.getSymbol();
if (sym) {
return {
name: sym.getName()
}
}
}).filter((p) => p !== undefined) as MethodDetails[];
return { className, properties, methods };
}
export function parseInterfaces(interfaceDeclaration: SimpleAST.InterfaceDeclaration) {
const interfaceName = interfaceDeclaration.getSymbol()!.getName();
const propertyDeclarations = interfaceDeclaration.getProperties();
const methodDeclarations = interfaceDeclaration.getMethods();
const properties = propertyDeclarations.map(property => {
const sym = property.getSymbol();
if (sym) {
return {
name: sym.getName()
}
}
}).filter((p) => p !== undefined) as PropertyDetails[];
const methods = methodDeclarations.map(method => {
const sym = method.getSymbol();
if (sym) {
return {
name: sym.getName()
}
}
}).filter((p) => p !== undefined) as MethodDetails[];
return { interfaceName, properties, methods };
}
export function parseHeritageClauses(classDeclaration: SimpleAST.ClassDeclaration) {
const className = classDeclaration.getSymbol()!.getName();
const extended = classDeclaration.getExtends();
const implemented = classDeclaration.getImplements();
let heritageClauses: HeritageClause[] = [];
if (extended) {
const identifier = extended.getChildrenOfKind(ts.SyntaxKind.Identifier)[0];
if (identifier) {
const sym = identifier.getSymbol();
if (sym) {
heritageClauses.push(
{
clause: sym.getName(),
className
}
);
}
}
}
if (implemented) {
implemented.forEach(i => {
const identifier = i.getChildrenOfKind(ts.SyntaxKind.Identifier)[0];
if (identifier) {
const sym = identifier.getSymbol();
if (sym) {
heritageClauses.push(
{
clause: sym.getName(),
className
}
);
}
}
});
}
return heritageClauses;
}
================================================
FILE: src/core/templates.ts
================================================
import { PropertyDetails, MethodDetails} from "./interfaces";
export const templates = {
composition: "+->",
implementsOrExtends: (abstraction: string, implementation: string) => {
return (
`${templates.plainClassOrInterface(abstraction)}` +
`^-${templates.plainClassOrInterface(implementation)}`
);
},
plainClassOrInterface: (name: string) => `[${name}]`,
colorClass: (name: string) => `[${name}{bg:skyblue}]`,
colorInterface: (name: string) => `[${name}{bg:palegreen}]`,
class: (name: string, props: PropertyDetails[], methods: MethodDetails[]) => {
const pTemplate = (property: PropertyDetails) => `${property.name};`;
const mTemplate = (method: MethodDetails) => `${method.name}();`;
return (
`${templates.colorClass(name)}` +
`[${name}|${props.map(pTemplate).join("")}|${methods.map(mTemplate).join("")}]`
);
},
interface: (
name: string,
props: PropertyDetails[],
methods: MethodDetails[]
) => {
const pTemplate = (property: PropertyDetails) => `${property.name};`;
const mTemplate = (method: MethodDetails) => `${method.name}();`;
return (
`${templates.colorInterface(name)}` +
`[${name}|${props.map(pTemplate).join("")}|${methods.map(mTemplate).join("")}]`
);
}
};
================================================
FILE: src/demo/interfaces.ts
================================================
export interface Weapon {
tryHit(fromDistance: number): boolean;
}
export interface Named {
name: string;
}
================================================
FILE: src/demo/katana.ts
================================================
import { Weapon, Named } from "./interfaces";
export class BaseWeapon {
damage = 25;
}
export class Katana extends BaseWeapon implements Weapon, Named {
name = "Katana";
public tryHit(fromDistance: number) {
return fromDistance <= 2;
}
}
================================================
FILE: src/demo/main.ts
================================================
import { Ninja } from "./ninja";
import { Katana } from "./katana";
const ninja = new Ninja(new Katana());
ninja.fight(5);
================================================
FILE: src/demo/ninja.ts
================================================
import { Weapon } from "./interfaces";
export class Ninja {
private _weapon: Weapon;
public constructor(weapon: Weapon) {
this._weapon = weapon;
}
public fight(fromDistance: number) {
return this._weapon.tryHit(fromDistance);
}
}
================================================
FILE: src/lib/index.ts
================================================
export { getUrl } from "../core";
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["dom", "es2015"],
"strict": true,
"outDir": "dist"
}
}