Skip to content

Commit 368e157

Browse files
committed
Added command ShowComponentHierarchy. The command generates a visual reprsentation of the Angular apps component hierarchy.
1 parent 2e28ed4 commit 368e157

8 files changed

+423
-4
lines changed

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"onCommand:angulartools.componentHierarchyDgml",
1414
"onCommand:angulartools.listAllImports",
1515
"onCommand:angulartools.projectDirectoryStructure",
16-
"onCommand:angulartools.packageJsonToMarkdown"
16+
"onCommand:angulartools.packageJsonToMarkdown",
17+
"onCommand:angulartools.showComponentHierarchy"
1718
],
1819
"main": "./out/extension.js",
1920
"contributes": {
@@ -33,7 +34,11 @@
3334
{
3435
"command": "angulartools.componentHierarchyDgml",
3536
"title": "AngularTools: Generate a Directed Graph file (dgml) showing the components in the current project."
36-
}
37+
},
38+
{
39+
"command": "angulartools.showComponentHierarchy",
40+
"title": "AngularTools: Show a graph representing the component hierarchy."
41+
}
3742
]
3843
},
3944
"scripts": {

src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './componentHierarchyDgml';
22
export * from './listAllImports';
33
export * from './packageJsonToMarkdown';
44
export * from './projectDirectoryStructure';
5+
export * from './showComponentHierarchy';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#network {
2+
position: absolute;
3+
top: 0px;
4+
right: 0px;
5+
bottom: 0px;
6+
left: 0px;
7+
display: block;
8+
border: 1px solid lightgray;
9+
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import * as vscode from 'vscode';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import { FileSystemUtils } from "../filesystemUtils";
5+
6+
class Component {
7+
8+
constructor(tsFilename: string, templateFilename: string, selector: string, subComponents: Component[], isRoot: boolean) {
9+
this.tsFilename = tsFilename;
10+
this.templateFilename = templateFilename;
11+
this.selector = selector;
12+
this.subComponents = subComponents;
13+
this.isRoot = isRoot;
14+
}
15+
public tsFilename: string;
16+
public templateFilename: string;
17+
public selector: string;
18+
public subComponents: Component[];
19+
public isRoot: boolean;
20+
}
21+
22+
class Node {
23+
constructor(id: string, tsFilename: string, isRoot: boolean) {
24+
this.id = id;
25+
this.tsFilename = tsFilename;
26+
this.isRoot = isRoot;
27+
}
28+
public id: string;
29+
public tsFilename: string;
30+
public isRoot: boolean;
31+
32+
public toJsonString(): string {
33+
return `{id: "${this.id}", label: "${this.id}"}`;
34+
}
35+
}
36+
class Edge {
37+
constructor(id: string, source: string, target: string) {
38+
this.id = id;
39+
this.source = source;
40+
this.target = target;
41+
}
42+
public id: string;
43+
public source: string;
44+
public target: string;
45+
46+
public toJsonString(): string {
47+
return `{from: "${this.source}", to: "${this.target}", arrows: arrowAttr }`;
48+
}
49+
}
50+
51+
export class ShowComponentHierarchy {
52+
53+
private extensionContext: vscode.ExtensionContext;
54+
constructor(context: vscode.ExtensionContext) {
55+
this.extensionContext = context;
56+
}
57+
public static get commandName(): string { return 'showComponentHierarchy'; }
58+
59+
public execute(webview: vscode.Webview) {
60+
const fsUtils = new FileSystemUtils();
61+
var directoryPath: string = fsUtils.getWorkspaceFolder();
62+
const excludeDirectories = ['bin', 'obj', 'node_modules', 'dist', 'packages', '.git', '.vs', '.github'];
63+
const componentFilenames = fsUtils.listFiles(directoryPath, excludeDirectories, this.isComponentFile);
64+
const components = this.findComponents(componentFilenames);
65+
this.enrichComponentsFromComponentTemplates(components);
66+
67+
let nodes: Node[] = [];
68+
const appendNodes = (nodeList: Node[]) => {
69+
nodeList.forEach(newNode => {
70+
if (!nodes.some(node => node.id === newNode.id)) {
71+
nodes = nodes.concat(newNode);
72+
}
73+
});
74+
};
75+
let edges: Edge[] = [];
76+
const appendLinks = (edgeList: Edge[]) => {
77+
edgeList.forEach(newEdge => {
78+
if (!edges.some(edge => edge.source === newEdge.source && edge.target === newEdge.target)) {
79+
edges = edges.concat(newEdge);
80+
}
81+
});
82+
};
83+
this.addNodesAndLinks(components, appendNodes, appendLinks);
84+
85+
const nodesJson = nodes
86+
.map((node, index, arr) => { return node.toJsonString(); })
87+
.join(',\n');
88+
const rootNodesJson = nodes
89+
.filter(node => node.isRoot)
90+
.map((node, index, arr) => { return '"' + node.id + '"'; })
91+
.join(',\n');
92+
const edgesJson = edges
93+
.map((edge, index, arr) => { return edge.toJsonString(); })
94+
.join(',\n');
95+
96+
try {
97+
const jsContent = this.generateJavascriptContent(nodesJson, rootNodesJson, edgesJson);
98+
const outputJsFilename = 'showComponentHierarchy.js';
99+
let htmlContent = this.generateHtmlContent(webview, outputJsFilename);
100+
101+
// fsUtils.writeFile(this.extensionContext?.asAbsolutePath(path.join('src', 'commands', 'showComponentHierarchy.html')), htmlContent, () => { } ); // For debugging
102+
fsUtils.writeFile(
103+
this.extensionContext?.asAbsolutePath(path.join('src', 'commands', outputJsFilename)),
104+
jsContent,
105+
() => {
106+
webview.html = htmlContent;
107+
}
108+
);
109+
} catch (ex) {
110+
console.log('Angular Tools Exception:' + ex);
111+
}
112+
}
113+
114+
private isComponentFile(filename: string): boolean {
115+
return filename.endsWith('.component.ts');
116+
}
117+
118+
private findComponents(componentFilenames: string[]) {
119+
const compHash: { [selector: string]: Component; } = {};
120+
const componentRegex = /@Component\({/ig;
121+
const templateUrlRegex = /.*templateUrl:.+\/(.+)\'/i;
122+
const selectorRegex = /.*selector:.+\'(.+)\'/i;
123+
const endBracketRegex = /}\)/i;
124+
componentFilenames.forEach((componentFilename) => {
125+
let componentDefinitionFound = false;
126+
let currentComponent = new Component(componentFilename, "", "", [], true);
127+
const content = fs.readFileSync(componentFilename, 'utf8');
128+
const lines: string[] = content.split('\n');
129+
for (let i: number = 0; i < lines.length; i++) {
130+
let line = lines[i];
131+
let match = componentRegex.exec(line);
132+
if (match) {
133+
componentDefinitionFound = true;
134+
}
135+
if (componentDefinitionFound) {
136+
match = templateUrlRegex.exec(line);
137+
if (match) {
138+
currentComponent.templateFilename = path.join(path.dirname(componentFilename), match[1]);
139+
}
140+
match = selectorRegex.exec(line);
141+
if (match) {
142+
let currentSelector = match[1];
143+
currentSelector = currentSelector.replace("[", "");
144+
currentSelector = currentSelector.replace("]", "");
145+
currentComponent.selector = currentSelector;
146+
}
147+
match = endBracketRegex.exec(line);
148+
if (match) {
149+
break;
150+
}
151+
}
152+
}
153+
compHash[currentComponent.selector] = currentComponent;
154+
});
155+
return compHash;
156+
}
157+
158+
private enrichComponentsFromComponentTemplates(componentHash: { [selector: string]: Component; }) {
159+
for (let selector1 in componentHash) {
160+
if (fs.existsSync(componentHash[selector1].templateFilename)) {
161+
const template = fs.readFileSync(componentHash[selector1].templateFilename); // We read the entire template file
162+
for (let selector2 in componentHash) { // then we check if the template contains each of the selectors we found in the components
163+
let pattern = `</${selector2}>`;
164+
let index = template.indexOf(pattern);
165+
if (index >= 0) {
166+
componentHash[selector1].subComponents = componentHash[selector1].subComponents.concat(componentHash[selector2]);
167+
// If selector2 has been found in a template then it is not root
168+
componentHash[selector2].isRoot = false;
169+
}
170+
else {
171+
pattern = ` ${selector2}`;
172+
index = template.indexOf(pattern);
173+
if (index >= 0) {
174+
componentHash[selector1].subComponents = componentHash[selector1].subComponents.concat(componentHash[selector2]);
175+
// If selector2 has been found in a template then it is not root
176+
componentHash[selector2].isRoot = false;
177+
}
178+
}
179+
}
180+
}
181+
}
182+
}
183+
184+
private addNodesAndLinks(componentHash: { [selector: string]: Component; }, appendNodes: (nodeList: Node[]) => void, appendLinks: (edgeList: Edge[]) => void) {
185+
for (let selector in componentHash) {
186+
const component = componentHash[selector];
187+
if (component.isRoot) {
188+
this.generateDirectedGraphNodesXml(component.subComponents, component, true, appendNodes);
189+
this.generateDirectedGraphLinksXml(component.subComponents, selector, "", appendLinks);
190+
}
191+
}
192+
}
193+
194+
private generateDirectedGraphNodesXml(components: Component[], component: Component, isRoot: boolean, appendNodes: (nodeList: Node[]) => void) {
195+
appendNodes([new Node(component.selector, component.templateFilename, isRoot)]);
196+
if (components.length > 0) {
197+
components.forEach((subComponent) => {
198+
this.generateDirectedGraphNodesXml(subComponent.subComponents, subComponent, subComponent.isRoot, appendNodes);
199+
});
200+
}
201+
}
202+
203+
private generateDirectedGraphLinksXml(subComponents: Component[], selector: string, parentSelector: string, appendLinks: (edgeList: Edge[]) => void) {
204+
if (parentSelector.length > 0) {
205+
const id = Math.random() * 100000;
206+
appendLinks([new Edge(id.toString(), parentSelector, selector)]);
207+
}
208+
if (subComponents.length > 0) {
209+
subComponents.forEach((subComponent) => {
210+
this.generateDirectedGraphLinksXml(subComponent.subComponents, subComponent.selector, selector, appendLinks);
211+
});
212+
}
213+
}
214+
215+
private getNonce() {
216+
let text = '';
217+
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
218+
for (let i = 0; i < 32; i++) {
219+
text += possible.charAt(Math.floor(Math.random() * possible.length));
220+
}
221+
return text;
222+
}
223+
224+
private generateJavascriptContent(nodesJson: string, rootNodesJson: string, edgesJson: string): string {
225+
const templateJsFilename = 'showComponentHierarchy_Template.js';
226+
let template = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('src', 'commands', templateJsFilename)), 'utf8');
227+
let jsContent = template.replace('var nodes = new vis.DataSet([]);', `var nodes = new vis.DataSet([${nodesJson}]);`);
228+
jsContent = jsContent.replace('var rootNodes = [];', `var rootNodes = [${rootNodesJson}];`);
229+
jsContent = jsContent.replace('var edges = new vis.DataSet([]);', `var edges = new vis.DataSet([${edgesJson}]);`);
230+
return jsContent;
231+
}
232+
233+
private generateHtmlContent(webview: vscode.Webview, outputJsFilename: string): string {
234+
const templateHtmlFilename = 'showComponentHierarchy_Template.html';
235+
let htmlContent = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('src', 'commands', templateHtmlFilename)), 'utf8');
236+
237+
const visPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'src', 'commands', 'vis-network.min.js');
238+
const visUri = webview.asWebviewUri(visPath);
239+
htmlContent = htmlContent.replace('vis-network.min.js', visUri.toString());
240+
241+
const cssPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'src', 'commands', 'showComponentHierarchy.css');
242+
const cssUri = webview.asWebviewUri(cssPath);
243+
htmlContent = htmlContent.replace('showComponentHierarchy.css', cssUri.toString());
244+
245+
const nonce = this.getNonce();
246+
htmlContent = htmlContent.replace('nonce-nonce', `nonce-${nonce}`);
247+
htmlContent = htmlContent.replace(/<script /g, `<script nonce="${nonce}" `);
248+
htmlContent = htmlContent.replace('cspSource', webview.cspSource);
249+
250+
const jsPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'src', 'commands', outputJsFilename);
251+
const jsUri = webview.asWebviewUri(jsPath);
252+
htmlContent = htmlContent.replace('showComponentHierarchy.js', jsUri.toString());
253+
return htmlContent;
254+
}
255+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<html>
2+
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src cspSource; script-src 'nonce-nonce';">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
8+
<link href="showComponentHierarchy.css" rel="stylesheet">
9+
10+
<script type="text/javascript" src="vis-network.min.js"></script>
11+
12+
</head>
13+
14+
<body>
15+
<div id="network">Testing...</div>
16+
<script type="text/javascript" src="showComponentHierarchy.js"></script>
17+
</body>
18+
19+
</html>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
(function () {
2+
var nodes = new vis.DataSet([]);
3+
4+
var rootNodes = [];
5+
rootNodes.forEach(nodeId => {
6+
nodes.get(nodeId).color = {
7+
background: "#00FF00"
8+
};
9+
});
10+
11+
var arrowAttr = {
12+
to: {
13+
enabled: true,
14+
type: "triangle"
15+
}
16+
};
17+
var edges = new vis.DataSet([]);
18+
19+
var data = {
20+
nodes: nodes,
21+
edges: edges
22+
};
23+
var options = {};
24+
var container = document.getElementById('network');
25+
var network = new vis.Network(container, data, options);
26+
27+
}());

src/commands/vis-network.min.js

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/extension.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
ComponentHierarchyDgml,
66
ListAllImports,
77
PackageJsonToMarkdown,
8-
ProjectDirectoryStructure
8+
ProjectDirectoryStructure,
9+
ShowComponentHierarchy
910
} from './commands';
1011

1112
export function activate(context: vscode.ExtensionContext) {
@@ -35,7 +36,23 @@ export function activate(context: vscode.ExtensionContext) {
3536
command.execute();
3637
});
3738
context.subscriptions.push(componentHierarchyDgmlDisposable);
38-
}
39+
40+
const showComponentHierarchyDisposable = vscode.commands.registerCommand(`${cmdPrefix}.${ShowComponentHierarchy.commandName}`, () => {
41+
const componentHierarchyPanel = vscode.window.createWebviewPanel(
42+
'angularTools_showComponentHierarchy',
43+
'Angular component hierarchy',
44+
vscode.ViewColumn.One,
45+
{
46+
enableScripts: true
47+
}
48+
);
49+
componentHierarchyPanel.onDidDispose(() => {
50+
51+
}, null, context.subscriptions );
52+
const command = new ShowComponentHierarchy(context);
53+
command.execute(componentHierarchyPanel.webview);
54+
});
55+
context.subscriptions.push(showComponentHierarchyDisposable);}
3956

4057
// this method is called when your extension is deactivated
4158
export function deactivate() { }

0 commit comments

Comments
 (0)