Skip to content

Commit 7717fbb

Browse files
committed
Added show module hierarchy command.
Refactoring show component hierarchy command.
1 parent 4666cef commit 7717fbb

File tree

7 files changed

+216
-44
lines changed

7 files changed

+216
-44
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Change Log
22

3+
## Version 1.5.0
4+
5+
- Added Show module hierarchy command that visualizes the module structure and their import and exports.
6+
- Improved the layout of nodes in the Show Component hierarchy command
7+
38
## Version 1.4.5
49

510
- Bugfix: The Package json to Markdown command failed if the node_modules folder did not exist.

src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export * from './modulesToMarkdown';
55
export * from './packageJsonToMarkdown';
66
export * from './projectDirectoryStructure';
77
export * from './showComponentHierarchy';
8+
export * from './showModuleHierarchy';
89
export * from './componentHierarchyMarkdown';

src/commands/showComponentHierarchy.ts

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import * as path from 'path';
55
import * as vscode from 'vscode';
66

77
export class ShowComponentHierarchy extends ShowHierarchyBase {
8-
private static readonly Name = 'showComponentHierarchy';
9-
public static get commandName(): string { return ShowComponentHierarchy.Name; }
8+
public static get commandName(): string { return 'showComponentHierarchy'; }
109

1110
public execute(webview: vscode.Webview) {
1211
this.checkForOpenWorkspace();
@@ -42,7 +41,7 @@ export class ShowComponentHierarchy extends ShowHierarchyBase {
4241

4342
try {
4443
const jsContent = this.generateJavascriptContent(nodesJson, rootNodesJson, edgesJson);
45-
const outputJsFilename = ShowComponentHierarchy.Name + '.js';
44+
const outputJsFilename = this.showComponentHierarchyJsFilename;
4645
let htmlContent = this.generateHtmlContent(webview, outputJsFilename);
4746

4847
//this.fsUtils.writeFile(this.extensionContext?.asAbsolutePath(path.join('out', ShowComponentHierarchy.Name + '.html')), htmlContent, () => { }); // For debugging
@@ -69,7 +68,7 @@ export class ShowComponentHierarchy extends ShowHierarchyBase {
6968
}
7069

7170
private generateDirectedGraphNodes(components: Component[], component: Component, isRoot: boolean, parentSelector: string, appendNodes: (nodeList: Node[]) => void) {
72-
appendNodes([new Node(component.selector, component.templateFilename, isRoot)]);
71+
appendNodes([new Node(component.selector, component.selector, isRoot)]);
7372
if (components.length > 0) {
7473
components.forEach((subComponent) => {
7574
if(parentSelector !== subComponent.selector) {
@@ -81,7 +80,7 @@ export class ShowComponentHierarchy extends ShowHierarchyBase {
8180

8281
private generateDirectedGraphEdges(subComponents: Component[], selector: string, parentSelector: string, appendLinks: (edgeList: Edge[]) => void) {
8382
if (parentSelector.length > 0) {
84-
const id = Math.random() * 100000;
83+
const id = this.edges.length;
8584
appendLinks([new Edge(id.toString(), parentSelector, selector)]);
8685
}
8786
if (subComponents.length > 0 && selector !== parentSelector) {
@@ -92,8 +91,7 @@ export class ShowComponentHierarchy extends ShowHierarchyBase {
9291
}
9392

9493
private generateJavascriptContent(nodesJson: string, rootNodesJson: string, edgesJson: string): string {
95-
const templateJsFilename = ShowComponentHierarchy.Name + '_Template.js';
96-
let template = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('templates', templateJsFilename)), 'utf8');
94+
let template = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('templates', this.templateJsFilename)), 'utf8');
9795
let jsContent = template.replace('var nodes = new vis.DataSet([]);', `var nodes = new vis.DataSet([${nodesJson}]);`);
9896
jsContent = jsContent.replace('var rootNodes = [];', `var rootNodes = [${rootNodesJson}];`);
9997
jsContent = jsContent.replace('var edges = new vis.DataSet([]);', `var edges = new vis.DataSet([${edgesJson}]);`);
@@ -106,27 +104,4 @@ export class ShowComponentHierarchy extends ShowHierarchyBase {
106104
jsContent = jsContent.replace('selectionCanvasContext.lineWidth = 2;', `selectionCanvasContext.lineWidth = ${this.config.graphSelectionWidth};`);
107105
return jsContent;
108106
}
109-
110-
private generateHtmlContent(webview: vscode.Webview, outputJsFilename: string): string {
111-
const templateHtmlFilename = ShowComponentHierarchy.Name + '_Template.html';
112-
let htmlContent = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('templates', templateHtmlFilename)), 'utf8');
113-
114-
const visPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'javascript', 'vis-network.min.js');
115-
const visUri = webview.asWebviewUri(visPath);
116-
htmlContent = htmlContent.replace('vis-network.min.js', visUri.toString());
117-
118-
const cssPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'stylesheet', ShowComponentHierarchy.Name + '.css');
119-
const cssUri = webview.asWebviewUri(cssPath);
120-
htmlContent = htmlContent.replace(ShowComponentHierarchy.Name + '.css', cssUri.toString());
121-
122-
const nonce = this.getNonce();
123-
htmlContent = htmlContent.replace('nonce-nonce', `nonce-${nonce}`);
124-
htmlContent = htmlContent.replace(/<script /g, `<script nonce="${nonce}" `);
125-
htmlContent = htmlContent.replace('cspSource', webview.cspSource);
126-
127-
const jsPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'out', outputJsFilename);
128-
const jsUri = webview.asWebviewUri(jsPath);
129-
htmlContent = htmlContent.replace(ShowComponentHierarchy.Name + '.js', jsUri.toString());
130-
return htmlContent;
131-
}
132107
}

src/commands/showHierarchyBase.ts

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,91 @@
11
import { CommandBase } from '@commands';
22
import { Config, FileSystemUtils } from '@src';
33
import { Base64 } from 'js-base64';
4+
import * as fs from 'fs';
45
import * as path from 'path';
56
import * as vscode from 'vscode';
67

8+
export enum NodeType {
9+
none,
10+
rootNode,
11+
component,
12+
module,
13+
pipe,
14+
directive
15+
}
716
export class Node {
8-
constructor(id: string, tsFilename: string, isRoot: boolean) {
17+
private config: Config = new Config();
18+
constructor(id: string, name: string, isRoot: boolean, nodeType: NodeType = NodeType.none) {
919
this.id = id;
10-
this.tsFilename = tsFilename;
20+
this.name = name;
1121
this.isRoot = isRoot;
22+
this.nodeType = nodeType;
1223
}
1324
public id: string;
14-
public tsFilename: string;
25+
public name: string;
1526
public isRoot: boolean;
27+
public nodeType: NodeType;
1628

1729
public toJsonString(): string {
18-
return `{id: "${this.id}", label: "${this.id}"}`;
30+
let nodeColorAttr = '';
31+
switch (this.nodeType) {
32+
case NodeType.rootNode:
33+
nodeColorAttr = `, color: "${this.config.rootNodeBackgroundColor}", shape: "${this.config.visNodeShape}"`;
34+
break;
35+
case NodeType.component:
36+
nodeColorAttr = `, color: "${this.config.componentNodeBackgroundColor}", shape: "${this.config.componentNodeShape}"`;
37+
break;
38+
case NodeType.module:
39+
nodeColorAttr = `, color: "${this.config.moduleNodeBackgroundColor}", shape: "${this.config.moduleNodeShape}"`;
40+
break;
41+
case NodeType.pipe:
42+
nodeColorAttr = `, color: "${this.config.pipeNodeBackgroundColor}", shape: "${this.config.pipeNodeShape}"`;
43+
break;
44+
case NodeType.directive:
45+
nodeColorAttr = `, color: "${this.config.directiveNodeBackgroundColor}", shape: "${this.config.directiveNodeShape}"`;
46+
break;
47+
default:
48+
nodeColorAttr = '';
49+
break;
50+
}
51+
const label = this.name.length > this.config.maximumNodeLabelLength ? this.name.substr(0, this.config.maximumNodeLabelLength) + '...' : this.name;
52+
return `{id: "${this.id}", label: "${label}" ${nodeColorAttr}}`;
1953
}
2054
}
2155

56+
export enum ArrowType {
57+
none = 0,
58+
import = 1,
59+
export = 2
60+
}
61+
2262
export class Edge {
23-
constructor(id: string, source: string, target: string) {
63+
private config: Config = new Config();
64+
constructor(id: string, source: string, target: string, arrowType: ArrowType = ArrowType.none) {
2465
this.id = id;
2566
this.source = source;
2667
this.target = target;
68+
this.arrowType = arrowType;
2769
}
2870
public id: string;
2971
public source: string;
3072
public target: string;
73+
public arrowType: ArrowType;
3174

3275
public toJsonString(): string {
33-
return `{from: "${this.source}", to: "${this.target}", arrows: arrowAttr }`;
76+
let arrowColorAttr = '';
77+
switch (this.arrowType) {
78+
case ArrowType.import:
79+
arrowColorAttr = `, color: "${this.config.importEdgeColor}"`;
80+
break;
81+
case ArrowType.export:
82+
arrowColorAttr = `, color: "${this.config.exportEdgeColor}"`;
83+
break;
84+
default:
85+
arrowColorAttr = '';
86+
break;
87+
}
88+
return `{from: "${this.source}", to: "${this.target}", arrows: arrowAttr ${arrowColorAttr} }`;
3489
}
3590
}
3691

@@ -40,6 +95,11 @@ export class ShowHierarchyBase extends CommandBase {
4095
protected extensionContext: vscode.ExtensionContext;
4196
protected nodes: Node[] = [];
4297
protected edges: Edge[] = [];
98+
protected templateJsFilename: string = 'showHierarchy_Template.js';
99+
protected templateHtmlFilename: string = 'showHierarchy_Template.html';
100+
protected showComponentHierarchyJsFilename: string = 'showComponentHierarchy.js';
101+
protected showModuleHierarchyJsFilename: string = 'showModuleHierarchy.js';
102+
protected showHierarchyCssFilename: string = 'showHierarchy.css';
43103

44104
constructor(context: vscode.ExtensionContext) {
45105
super();
@@ -76,10 +136,32 @@ export class ShowHierarchyBase extends CommandBase {
76136

77137
const workspaceDirectory = this.fsUtils.getWorkspaceFolder();
78138
const newFilePath = path.join(workspaceDirectory, pngFilename);
79-
this.fsUtils.writeFile(newFilePath, u8arr, () => {});
139+
this.fsUtils.writeFile(newFilePath, u8arr, () => { });
80140

81141
vscode.window.showInformationMessage(`The file ${pngFilename} has been created in the root of the workspace.`);
82142
}
83143
}
84144

145+
146+
protected generateHtmlContent(webview: vscode.Webview, outputJsFilename: string): string {
147+
let htmlContent = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('templates', this.templateHtmlFilename)), 'utf8');
148+
149+
const visPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'javascript', 'vis-network.min.js');
150+
const visUri = webview.asWebviewUri(visPath);
151+
htmlContent = htmlContent.replace('vis-network.min.js', visUri.toString());
152+
153+
const cssPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'stylesheet', this.showHierarchyCssFilename);
154+
const cssUri = webview.asWebviewUri(cssPath);
155+
htmlContent = htmlContent.replace(this.showHierarchyCssFilename, cssUri.toString());
156+
157+
const nonce = this.getNonce();
158+
htmlContent = htmlContent.replace('nonce-nonce', `nonce-${nonce}`);
159+
htmlContent = htmlContent.replace(/<script /g, `<script nonce="${nonce}" `);
160+
htmlContent = htmlContent.replace('cspSource', webview.cspSource);
161+
162+
const jsPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'out', outputJsFilename);
163+
const jsUri = webview.asWebviewUri(jsPath);
164+
htmlContent = htmlContent.replace('showHierarchy.js', jsUri.toString());
165+
return htmlContent;
166+
}
85167
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Node, Edge, ShowHierarchyBase, NodeType, ArrowType } from './showHierarchyBase';
2+
import { ModuleManager, Project } from '@src';
3+
import * as fs from 'fs';
4+
import * as path from 'path';
5+
import * as vscode from 'vscode';
6+
7+
export class ShowModuleHierarchy extends ShowHierarchyBase {
8+
static get commandName() { return 'showModuleHierarchy'; }
9+
public execute(webview: vscode.Webview) {
10+
this.checkForOpenWorkspace();
11+
webview.onDidReceiveMessage(
12+
message => {
13+
switch (message.command) {
14+
case 'saveAsPng':
15+
this.saveAsPng(this.config.moduleHierarchyFilename, message.text);
16+
return;
17+
}
18+
},
19+
undefined,
20+
this.extensionContext.subscriptions
21+
);
22+
var workspaceFolder = this.fsUtils.getWorkspaceFolder();
23+
const errors: string[] = [];
24+
const project: Project = ModuleManager.scanProject(workspaceFolder, errors, this.isTypescriptFile);
25+
26+
this.nodes = [];
27+
this.edges = [];
28+
this.addNodesAndEdges(project, this.appendNodes, this.appendEdges);
29+
const nodesJson = this.nodes
30+
.map((node, index, arr) => { return node.toJsonString(); })
31+
.join(',\n');
32+
const edgesJson = this.edges
33+
.map((edge, index, arr) => { return edge.toJsonString(); })
34+
.join(',\n');
35+
36+
try {
37+
const jsContent = this.generateJavascriptContent(nodesJson, edgesJson);
38+
const outputJsFilename = this.showModuleHierarchyJsFilename;
39+
let htmlContent = this.generateHtmlContent(webview, this.showModuleHierarchyJsFilename);
40+
//this.fsUtils.writeFile(this.extensionContext?.asAbsolutePath(path.join('out', ShowComponentHierarchy.Name + '.html')), htmlContent, () => { }); // For debugging
41+
this.fsUtils.writeFile(
42+
this.extensionContext?.asAbsolutePath(path.join('out', outputJsFilename)),
43+
jsContent,
44+
() => {
45+
webview.html = htmlContent;
46+
}
47+
);
48+
}
49+
catch (ex) {
50+
console.log('Angular Tools Exception:' + ex);
51+
}
52+
if (errors.length > 0) {
53+
this.showErrors(errors);
54+
}
55+
}
56+
addNodesAndEdges(project: Project, appendNodes: (nodeList: Node[]) => void, appendEdges: (edgeList: Edge[]) => void) {
57+
project.modules.forEach(module => {
58+
appendNodes([new Node(module.moduleName, module.moduleName, false, NodeType.module)]);
59+
module.imports.forEach(_import => {
60+
const nodeType = this.getNodeType(project, _import);
61+
appendNodes([new Node(_import, _import, false, nodeType)]);
62+
appendEdges([new Edge((this.edges.length + 1).toString(), _import, module.moduleName, ArrowType.import)]);
63+
});
64+
module.exports.forEach(_export => {
65+
const nodeType = this.getNodeType(project, _export);
66+
appendNodes([new Node(_export, _export, false, nodeType)]);
67+
appendEdges([new Edge((this.edges.length + 1).toString(), module.moduleName, _export, ArrowType.export)]);
68+
});
69+
});
70+
}
71+
getNodeType(project: Project, className: string) {
72+
let nodeType = NodeType.none;
73+
if (project.moduleNames.has(className) || className.endsWith('Module') || className.includes("Module.")) {
74+
nodeType = NodeType.module;
75+
}
76+
else if (project.components.has(className) || className.endsWith('Component')) {
77+
nodeType = NodeType.component;
78+
}
79+
else if (project.directives.has(className) || className.endsWith('Directive')) {
80+
nodeType = NodeType.directive;
81+
}
82+
else if (project.pipes.has(className) || className.endsWith('Pipe')) {
83+
nodeType = NodeType.pipe;
84+
}
85+
return nodeType;
86+
}
87+
generateJavascriptContent(nodesJson: string, edgesJson: string) {
88+
let template = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('templates', this.templateJsFilename)), 'utf8');
89+
let jsContent = template.replace('var nodes = new vis.DataSet([]);', `var nodes = new vis.DataSet([${nodesJson}]);`);
90+
jsContent = jsContent.replace('var edges = new vis.DataSet([]);', `var edges = new vis.DataSet([${edgesJson}]);`);
91+
jsContent = jsContent.replace('type: "triangle" // edge arrow to type', `type: "${this.config.visEdgeArrowToType}" // edge arrow to type}`);
92+
jsContent = jsContent.replace('ctx.strokeStyle = \'blue\'; // graph selection guideline color', `ctx.strokeStyle = '${this.config.graphSelectionGuidelineColor}'; // graph selection guideline color`);
93+
jsContent = jsContent.replace('ctx.lineWidth = 1; // graph selection guideline width', `ctx.lineWidth = ${this.config.graphSelectionGuidelineWidth}; // graph selection guideline width`);
94+
jsContent = jsContent.replace('selectionCanvasContext.strokeStyle = \'red\';', `selectionCanvasContext.strokeStyle = '${this.config.graphSelectionColor}';`);
95+
jsContent = jsContent.replace('selectionCanvasContext.lineWidth = 2;', `selectionCanvasContext.lineWidth = ${this.config.graphSelectionWidth};`);
96+
return jsContent;
97+
}
98+
99+
showErrors(errors: string[]) {
100+
const angularToolsOutput = vscode.window.createOutputChannel(this.config.angularToolsOutputChannel);
101+
angularToolsOutput.clear();
102+
angularToolsOutput.appendLine(`Parsing of ${errors.length > 1 ? 'some' : 'one'} of the modules failed.\n`);
103+
angularToolsOutput.appendLine('Below is a list of the errors.');
104+
angularToolsOutput.appendLine(errors.join('\n'));
105+
angularToolsOutput.show();
106+
}
107+
}

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
PackageJsonToMarkdown,
1111
ProjectDirectoryStructure,
1212
ShowComponentHierarchy,
13+
ShowModuleHierarchy
1314
} from '@commands';
14-
import { ShowModuleHierarchy } from './commands/showModuleHierarchy';
1515

1616
export function activate(context: vscode.ExtensionContext) {
1717

src/moduleManager.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ class Pipe extends NamedEntity { }
3333
class Component extends NamedEntity { }
3434
export class Project {
3535
public modules: NgModule[] = [];
36-
public components: string[] = [];
37-
public pipes: string[] = [];
38-
public directives: string[] = [];
36+
public moduleNames: Map<string, string> = new Map<string, string>();
37+
public components: Map<string, string> = new Map<string, string>();
38+
public pipes: Map<string, string> = new Map<string, string>();
39+
public directives: Map<string, string> = new Map<string, string>();
3940
}
4041

4142
export class ModuleManager {
@@ -49,15 +50,16 @@ export class ModuleManager {
4950
const file = this.readTypescriptFile(filename, errors);
5051
if (file instanceof NgModule) {
5152
project.modules.push(file as NgModule);
53+
project.moduleNames.set(file.moduleName, file.moduleName);
5254
}
5355
else if (file instanceof Component) {
54-
project.components.push(file.name);
56+
project.components.set(file.name, file.name);
5557
}
5658
else if (file instanceof Pipe) {
57-
project.pipes.push(file.name);
59+
project.pipes.set(file.name, file.name);
5860
}
5961
else if (file instanceof Directive) {
60-
project.directives.push(file.name);
62+
project.directives.set(file.name, file.name);
6163
}
6264
});
6365
return project;

0 commit comments

Comments
 (0)