, noteType: NodeType, arrowType: ArrowType) {
128 | namedEntityMap.forEach(namedEntity => {
129 | const entityPosition = this.graphState.nodePositions[namedEntity.name];
130 | this.appendNodes([new Node(namedEntity.name, this.getNodeLabel(namedEntity), this.fixTsFilename(namedEntity.filename), namedEntity.filename, false, noteType, entityPosition)]);
131 | namedEntity.dependencies.forEach(dependency => {
132 | const dependencyPosition = this.graphState.nodePositions[dependency.name];
133 | this.appendNodes([new Node(dependency.name, dependency.name, this.fixTsFilename(dependency.filename), dependency.filename, false, NodeType.injectable, dependencyPosition)]);
134 | this.appendEdges([new Edge((this.edges.length + 1).toString(), dependency.name, namedEntity.name, arrowType)]);
135 | });
136 | });
137 | }
138 |
139 | private fixTsFilename(filename: string): string {
140 | let entityFilename = filename.replace(this.workspaceDirectory, '.');
141 | entityFilename = entityFilename.split('\\').join('/');
142 | return entityFilename;
143 | }
144 |
145 | private generateJavascriptContent(nodesJson: string, edgesJson: string) {
146 | let template = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('templates', this.templateJsFilename)), 'utf8');
147 | let jsContent = template.replace('const nodes = new vis.DataSet([]);', `var nodes = new vis.DataSet([${nodesJson}]);`);
148 | jsContent = jsContent.replace('const edges = new vis.DataSet([]);', `var edges = new vis.DataSet([${edgesJson}]);`);
149 | jsContent = jsContent.replace('type: "triangle" // edge arrow to type', `type: "${this.config.dependencyInjectionEdgeArrowToType}" // edge arrow to type}`);
150 | jsContent = jsContent.replace('ctx.strokeStyle = \'blue\'; // graph selection guideline color', `ctx.strokeStyle = '${this.config.graphSelectionGuidelineColor}'; // graph selection guideline color`);
151 | jsContent = jsContent.replace('ctx.lineWidth = 1; // graph selection guideline width', `ctx.lineWidth = ${this.config.graphSelectionGuidelineWidth}; // graph selection guideline width`);
152 | jsContent = jsContent.replace('selectionCanvasContext.strokeStyle = \'red\';', `selectionCanvasContext.strokeStyle = '${this.config.graphSelectionColor}';`);
153 | jsContent = jsContent.replace('selectionCanvasContext.lineWidth = 2;', `selectionCanvasContext.lineWidth = ${this.config.graphSelectionWidth};`);
154 | jsContent = jsContent.replace('let showHierarchicalOptionsCheckboxChecked = false;', `let showHierarchicalOptionsCheckboxChecked = ${this.graphState.showHierarchicalOptions};`);
155 | jsContent = jsContent.replace('let hierarchicalOptionsDirectionSelectValue = undefined;', `let hierarchicalOptionsDirectionSelectValue = '${this.graphState.graphDirection}';`);
156 | jsContent = jsContent.replace('let hierarchicalOptionsSortMethodSelectValue = undefined;', `let hierarchicalOptionsSortMethodSelectValue = '${this.graphState.graphLayout}';`);
157 | jsContent = this.setGraphState(jsContent);
158 | return jsContent;
159 | }
160 | }
--------------------------------------------------------------------------------
/src/commands/index.ts:
--------------------------------------------------------------------------------
1 | export * from './commandBase';
2 | export * from './listAllImports';
3 | export * from './modulesToMarkdown';
4 | export * from './packageJsonToMarkdown';
5 | export * from './projectDirectoryStructure';
6 | export * from './showComponentHierarchy';
7 | export * from './showModuleHierarchy';
8 | export * from './componentHierarchyMarkdown';
9 | export * from './generateDependencyInjectionGraph';
10 |
--------------------------------------------------------------------------------
/src/commands/listAllImports.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as vscode from 'vscode';
3 |
4 | import { ArrayUtils, Config, FileSystemUtils } from '@src';
5 | import { CommandBase } from '@commands';
6 | import { Settings } from '@model';
7 |
8 | export class ListAllImports extends CommandBase {
9 | private config = new Config();
10 | public static get commandName(): string { return 'listAllImports'; }
11 |
12 | public execute() {
13 | this.checkForOpenWorkspace();
14 | const fsUtils = new FileSystemUtils();
15 | var workspaceFolder: string = fsUtils.getWorkspaceFolder();
16 | const settings: Settings = fsUtils.readProjectSettings(this.config);
17 | const files = fsUtils.listFiles(workspaceFolder, settings, this.isTypescriptFile);
18 | this.writeResult(workspaceFolder, files);
19 | }
20 |
21 | private writeResult(workspaceDirectory: string, results: string[]) {
22 | const imports: { [module: string]: number } = {};
23 | if (!results) { return; }
24 | for (let i = 0; i < results.length; i++) {
25 | var file = results[i];
26 | const regexImports: RegExp = new RegExp('.*?\\s+from\\s+[\'"](.+)[\'"]', 'ig');
27 | const regexRequires: RegExp = new RegExp('.*?\\s+require\\s*\\(\\s*[\'"](.+)[\'"]\\s*\\)', 'ig');
28 | const content = fs.readFileSync(file, 'utf8');
29 | const lines: string[] = content.split('\n');
30 | lines.forEach(line => {
31 | const matchImports = regexImports.exec(line);
32 | const matchRequires = regexRequires.exec(line);
33 | if (matchImports || matchRequires) {
34 | let key: string = '';
35 | if (matchImports) {
36 | key = matchImports[1];
37 | }
38 | if (matchRequires) {
39 | key = matchRequires[1];
40 | }
41 | if (Object.prototype.hasOwnProperty.call(imports, key)) {
42 | imports[key] = imports[key] + 1;
43 | } else {
44 | imports[key] = 1;
45 | }
46 | }
47 | });
48 | }
49 |
50 | const angularToolsOutput = vscode.window.createOutputChannel(this.config.angularToolsOutputChannel);
51 | angularToolsOutput.clear();
52 | angularToolsOutput.appendLine(`Imports for files in workspace: ${workspaceDirectory}`);
53 | angularToolsOutput.appendLine('The number following each import in the list is the number of occurrences of the package import.\n');
54 | for (const key of Object.keys(imports).sort(ArrayUtils.sortStrings)) {
55 | angularToolsOutput.appendLine(`${key}: ${imports[key]}`);
56 | }
57 | angularToolsOutput.show();
58 | }
59 |
60 | }
--------------------------------------------------------------------------------
/src/commands/modulesToMarkdown.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as path from 'path';
3 | import { ArrayUtils, Config, FileSystemUtils, ModuleManager } from '@src';
4 | import { CommandBase } from '@commands';
5 | import { NgModule, Project } from '@model';
6 |
7 | export class ModulesToMarkdown extends CommandBase {
8 | private config = new Config();
9 | public static get commandName(): string { return 'modulesToMarkdown'; }
10 |
11 | public execute() {
12 | this.checkForOpenWorkspace();
13 | const fsUtils = new FileSystemUtils();
14 | var workspaceDirectory: string = fsUtils.getWorkspaceFolder();
15 | let markdownContent = '# Modules\n\n';
16 | const errors: string[] = [];
17 | const project: Project = ModuleManager.scanProject(workspaceDirectory, errors, this.isTypescriptFile);
18 | markdownContent = markdownContent +
19 | '## Modules in workspace\n\n' +
20 | '| Module | Declarations | Imports | Exports | Bootstrap | Providers | Entry points |\n' +
21 | '| ---| --- | --- | --- | --- | --- | --- |\n';
22 | let modulesMarkdown: string = '';
23 | project.modules.forEach(module => {
24 | markdownContent = markdownContent + '| ' + module.moduleName + ' | ' + module.moduleStats().join(' | ') + ' |\n';
25 | modulesMarkdown = modulesMarkdown + this.generateModuleMarkdown(module);
26 | });
27 | markdownContent = markdownContent + '\n' + modulesMarkdown;
28 | if (errors.length > 0) {
29 | this.showErrors(errors);
30 | }
31 | fsUtils.writeFileAndOpen(path.join(workspaceDirectory, this.config.modulesToMarkdownFilename), markdownContent);
32 | }
33 |
34 | private generateModuleMarkdown(module: NgModule): string {
35 | let markdown = `## ${module.moduleName}\n\n`;
36 | markdown = markdown +
37 | 'Filename: ' + module.filename + '\n\n' +
38 | '| Section | Classes, service, modules |\n' +
39 | '| ---- |:-----------|\n' +
40 | '| Declarations | ' + ArrayUtils.arrayToMarkdown(module.declarations) + ' |\n' +
41 | '| Imports | ' + ArrayUtils.arrayToMarkdown(module.imports) + ' |\n' +
42 | '| Exports | ' + ArrayUtils.arrayToMarkdown(module.exports) + ' |\n' +
43 | '| Bootstrap | ' + ArrayUtils.arrayToMarkdown(module.bootstrap) + ' |\n' +
44 | '| Providers | ' + ArrayUtils.arrayToMarkdown(module.providers) + ' |\n' +
45 | '| Entry components | ' + ArrayUtils.arrayToMarkdown(module.entryComponents) + ' |\n' +
46 | '\n';
47 |
48 | return markdown;
49 | }
50 |
51 | private showErrors(errors: string[]) {
52 | const angularToolsOutput = vscode.window.createOutputChannel(this.config.angularToolsOutputChannel);
53 | angularToolsOutput.clear();
54 | angularToolsOutput.appendLine(`Parsing of ${errors.length > 1 ? 'some' : 'one'} of the modules failed.\n`);
55 | angularToolsOutput.appendLine('Below is a list of the errors.');
56 | angularToolsOutput.appendLine(errors.join('\n'));
57 | angularToolsOutput.show();
58 | }
59 | }
--------------------------------------------------------------------------------
/src/commands/packageJsonToMarkdown.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 |
4 | import { ArrayUtils, Config, FileSystemUtils } from '@src';
5 | import { CommandBase } from '@commands';
6 |
7 | const fetch = require('npm-registry-fetch');
8 |
9 | export class PackageJsonToMarkdown extends CommandBase {
10 | private config = new Config();
11 | public static get commandName(): string { return 'packageJsonToMarkdown'; }
12 |
13 | public execute() {
14 | this.checkForOpenWorkspace();
15 | const fsUtils = new FileSystemUtils();
16 | const directoryPath: string = fsUtils.getWorkspaceFolder();
17 | const projectSettings = fsUtils.readProjectSettings(this.config);
18 | const isPackageJson = (filename: string): boolean => filename.toLowerCase().endsWith('package.json');
19 | const files = fsUtils.listFiles(directoryPath, projectSettings, isPackageJson);
20 | this.writeMarkdownFile(directoryPath, files);
21 | }
22 |
23 | private writeMarkdownFile(workspaceDirectory: string, packageJsonFiles: string[]) {
24 | let devDependencies: string[] = [];
25 | let dependencies: string[] = [];
26 | let peerDependencies: string[] = [];
27 | const localPackages: { [pkgName: string]: string; } = {};
28 | packageJsonFiles.forEach(packageJsonFile => {
29 | const contents = fs.readFileSync(packageJsonFile).toString('utf8');
30 | const packageJson = JSON.parse(contents);
31 | if (packageJson.devDependencies) {
32 | devDependencies = [...new Set([...devDependencies, ...Object.keys(packageJson.devDependencies)])];
33 | this.updateLocalPackagesDictionary(packageJson.devDependencies, localPackages);
34 | }
35 | if (packageJson.dependencies) {
36 | dependencies = [...new Set([...dependencies, ...Object.keys(packageJson.dependencies)])];
37 | this.updateLocalPackagesDictionary(packageJson.dependencies, localPackages);
38 | }
39 | if (packageJson.peerDependencies) {
40 | peerDependencies = [...new Set([...peerDependencies, ...Object.keys(packageJson.peerDependencies)])];
41 | this.updateLocalPackagesDictionary(packageJson.peerDependencies, localPackages);
42 | }
43 | });
44 |
45 | let dependenciesMarkdown = '';
46 | let devDependenciesMarkdown = '';
47 | let peerDependenciesMarkdown = '';
48 | const dependenciesRequests: Promise<{ name: string, version: string, description: string, license: string }>[] = [];
49 | dependencies.sort(ArrayUtils.sortStrings).forEach(pckName => {
50 | dependenciesRequests.push(this.fetchPackageInformation(pckName, workspaceDirectory));
51 | });
52 | Promise.all(dependenciesRequests).then(responses => {
53 | dependenciesMarkdown = this.updateMarkdownRow(responses, localPackages);
54 | }).then(() => {
55 | const devDependenciesRequests: Promise<{ name: string, version: string, description: string, license: string }>[] = [];
56 | devDependencies.sort(ArrayUtils.sortStrings).forEach(pckName => {
57 | devDependenciesRequests.push(this.fetchPackageInformation(pckName, workspaceDirectory));
58 | });
59 | Promise.all(devDependenciesRequests).then(responses => {
60 | devDependenciesMarkdown = this.updateMarkdownRow(responses, localPackages);
61 | }).then(() => {
62 | const peerDependenciesRequests: Promise<{ name: string, version: string, description: string, license: string }>[] = [];
63 | peerDependencies.sort(ArrayUtils.sortStrings).forEach(pckName => {
64 | peerDependenciesRequests.push(this.fetchPackageInformation(pckName, workspaceDirectory));
65 | });
66 | Promise.all(peerDependenciesRequests).then(responses => {
67 | peerDependenciesMarkdown = this.updateMarkdownRow(responses, localPackages);
68 | }).then(() => {
69 | if (dependenciesMarkdown === '') {
70 | dependenciesMarkdown = 'No dependencies specified.';
71 | } else {
72 | dependenciesMarkdown =
73 | '| Name | Local version | Latest Version | License | Description|\n' +
74 | '| ---- | ---- | ---- | ---- |:-----------|\n' +
75 | dependenciesMarkdown;
76 | }
77 | if (devDependenciesMarkdown === '') {
78 | devDependenciesMarkdown = 'No dev dependencies specified.';
79 | } else {
80 | devDependenciesMarkdown =
81 | '| Name | Local version | Latest Version | License | Description|\n' +
82 | '| ---- | ---- | ---- | ---- |:-----------|\n' +
83 | devDependenciesMarkdown;
84 | }
85 | if (peerDependenciesMarkdown === '') {
86 | peerDependenciesMarkdown = 'No peer dependencies specified.';
87 | } else {
88 | peerDependenciesMarkdown =
89 | '| Name | Local version | Latest Version | License | Description|\n' +
90 | '| ---- | ---- | ---- | ---- |:-----------|\n' +
91 | peerDependenciesMarkdown;
92 | }
93 | const markdownContent =
94 | '# Package.json\n\n' +
95 | '## Dependencies\n\n' +
96 | dependenciesMarkdown + '\n' +
97 | '## Dev dependencies\n\n' +
98 | devDependenciesMarkdown + '\n' +
99 | '## Peer dependencies\n\n' +
100 | peerDependenciesMarkdown + '\n';
101 | const fsUtils = new FileSystemUtils();
102 | fsUtils.writeFileAndOpen(path.join(workspaceDirectory, this.config.packageJsonMarkdownFilename), markdownContent);
103 | });
104 | });
105 | });
106 | }
107 |
108 | private updateMarkdownRow(responses: { name: string; version: string; description: string; license: string }[], localPackages: { [pkgName: string]: string; }): string {
109 | let markdownStr: string = '';
110 | responses.sort((first, second) => (first.name.replace('@', '') < second.name.replace('@', '') ? -1 : 1)).forEach(response => {
111 | if (response) {
112 | const localVersion = localPackages[response.name];
113 | markdownStr += `| ${response.name} | ${localVersion} | ${response.version} | ${response.license} | ${response.description} |\n`;
114 | }
115 | });
116 | return markdownStr;
117 | }
118 |
119 | private updateLocalPackagesDictionary(dict: { [pkgName: string]: string; }, localPackages: { [pkgName: string]: string; }) {
120 | Object.entries(dict).forEach(([pkgName, version]) => {
121 | if (localPackages[pkgName] !== undefined) {
122 | if (!localPackages[pkgName].includes(String(version))) {
123 | localPackages[pkgName] += ', ' + String(version);
124 | }
125 | } else {
126 | localPackages[pkgName] = String(version);
127 | }
128 | });
129 | }
130 |
131 | private fetchPackageInformation(pckName: string, workspaceDirectory: string): Promise<{ name: string, version: string, description: string, license: string }> {
132 | const uri = `/${pckName}`;
133 | const license = this.getLicenseInformationFromNodeModulesFolder(workspaceDirectory, pckName);
134 | const result = fetch.json(uri)
135 | .then((json: any) => {
136 | let packageName = json.name;
137 | packageName = packageName?.replace('|', '|');
138 | let packageDescription = json.description;
139 | packageDescription = packageDescription?.replace('|', '|');
140 | let packageVersion = json['dist-tags'].latest;
141 | packageVersion = packageVersion?.replace('|', '|');
142 | return { name: packageName, description: packageDescription, version: packageVersion, license: license };
143 | })
144 | .catch(() => {
145 | return { name: pckName, description: 'N/A', version: 'N/A', license: license };
146 | });
147 | return result;
148 | }
149 |
150 | private getLicenseInformationFromNodeModulesFolder(workspaceDirectory: string, pckName: string): string {
151 | const pckFolder = path.join(workspaceDirectory, 'node_modules', pckName);
152 | const packageJsonFile = path.join(pckFolder, 'package.json');
153 | let packageJson = undefined;
154 | try {
155 | const contents = fs.readFileSync(packageJsonFile).toString('utf8');
156 | packageJson = JSON.parse(contents);
157 | }
158 | catch {
159 | packageJson = undefined;
160 | }
161 | if (packageJson !== undefined && packageJson.license) {
162 | return packageJson.license;
163 | } else {
164 | return 'N/A';
165 | }
166 | }
167 | }
--------------------------------------------------------------------------------
/src/commands/projectDirectoryStructure.ts:
--------------------------------------------------------------------------------
1 | import { Config, FileSystemUtils } from '@src';
2 | import { CommandBase } from '@commands';
3 | import * as vscode from 'vscode';
4 | import { Settings } from '@model';
5 |
6 | export class ProjectDirectoryStructure extends CommandBase {
7 | private config = new Config();
8 | public static get commandName(): string { return 'projectDirectoryStructure'; }
9 |
10 | public execute() {
11 | this.checkForOpenWorkspace();
12 | const fsUtils = new FileSystemUtils();
13 | var workspaceFolder: string = fsUtils.getWorkspaceFolder();
14 | const settings: Settings = fsUtils.readProjectSettings(this.config);
15 | const directories: string[] = fsUtils.listDirectories(workspaceFolder, settings);
16 | this.writeDirectoryStructure(workspaceFolder, this.config.projectDirectoryStructureMarkdownFilename, directories);
17 | }
18 |
19 | private writeDirectoryStructure(workspaceFolder: string, filename: string, directories: string[]) {
20 | const angularToolsOutput = vscode.window.createOutputChannel(this.config.angularToolsOutputChannel);
21 | angularToolsOutput.clear();
22 | angularToolsOutput.appendLine('Project Directory Structure');
23 | angularToolsOutput.appendLine(`Workspace directory: ${workspaceFolder}\n`);
24 | angularToolsOutput.appendLine('Directories:');
25 | directories?.forEach(directoryFullPath => {
26 | var directoryName = directoryFullPath.replace(workspaceFolder, '.');
27 | angularToolsOutput.appendLine(directoryName);
28 | });
29 | angularToolsOutput.show();
30 | }
31 | }
--------------------------------------------------------------------------------
/src/commands/showComponentHierarchy.ts:
--------------------------------------------------------------------------------
1 | import { ShowHierarchyBase } from './showHierarchyBase';
2 | import { ComponentManager, FileSystemUtils } from '@src';
3 | import { ArrowType, Component, Edge, GraphState, Node, NodeType, Settings } from '@model';
4 | import * as fs from 'fs';
5 | import * as path from 'path';
6 | import * as vscode from 'vscode';
7 |
8 | export class ShowComponentHierarchy extends ShowHierarchyBase {
9 | public static get commandName(): string { return 'showComponentHierarchy'; }
10 |
11 | private workspaceFolder: string = this.fsUtils.getWorkspaceFolder();
12 |
13 | public execute(webview: vscode.Webview) {
14 | this.checkForOpenWorkspace();
15 | webview.onDidReceiveMessage(
16 | message => {
17 | switch (message.command) {
18 | case 'saveAsPng': {
19 | this.saveAsPng(this.config.componentHierarchyPngFilename, message.text);
20 | return;
21 | }
22 | case 'saveAsDgml': {
23 | this.saveAsDgml(this.config.componentHierarchyDgmlGraphFilename, message.text, `'The component hierarchy has been analyzed and a Directed Graph Markup Language (dgml) file '${this.config.componentHierarchyDgmlGraphFilename}' has been created'`);
24 | return;
25 | }
26 | case 'saveAsDot': {
27 | this.saveAsDot(this.config.componentHierarchyDotGraphFilename, message.text, 'componentHierarchy', `'The component hierarchy has been analyzed and a GraphViz (dot) file '${this.config.componentHierarchyDotGraphFilename}' has been created'`);
28 | return;
29 | }
30 | case 'setGraphState': {
31 | const newGraphState: GraphState = JSON.parse(message.text);
32 | this.graphState = newGraphState;
33 | this.setNewState(this.graphState);
34 | this.nodes.forEach(node => {
35 | node.position = this.graphState.nodePositions[node.id];
36 | });
37 | this.addNodesAndEdges(components, this.appendNodes, this.appendEdges);
38 | this.generateAndSaveJavascriptContent(() => { });
39 | return;
40 | }
41 | case 'openFile': {
42 | const filename = message.text;
43 | if (this.fsUtils.fileExists(filename)) {
44 | var openPath = vscode.Uri.parse("file:///" + filename);
45 | vscode.workspace.openTextDocument(openPath).then(doc => {
46 | vscode.window.showTextDocument(doc);
47 | });
48 | }
49 | return;
50 | }
51 | }
52 | },
53 | undefined,
54 | this.extensionContext.subscriptions
55 | );
56 |
57 | const fsUtils = new FileSystemUtils();
58 | const settings: Settings = fsUtils.readProjectSettings(this.config);
59 | const components = ComponentManager.scanWorkspaceForComponents(this.workspaceFolder, settings);
60 |
61 | this.nodes = [];
62 | this.edges = [];
63 | this.addNodesAndEdges(components, this.appendNodes, this.appendEdges);
64 | const htmlContent = this.generateHtmlContent(webview, this.showComponentHierarchyJsFilename);
65 | //this.fsUtils.writeFile(this.extensionContext?.asAbsolutePath(path.join('out', ShowComponentHierarchy.Name + '.html')), htmlContent, () => { }); // For debugging
66 | this.generateAndSaveJavascriptContent(() => { webview.html = htmlContent; });
67 | }
68 |
69 | private generateAndSaveJavascriptContent(callback: () => any) {
70 | const nodesJson = this.nodes
71 | .map(node => { return node.toJsonString(); })
72 | .join(',\n');
73 | const rootNodesJson = this.nodes
74 | .filter(node => node.isRoot)
75 | .map(node => { return '"' + node.id + '"'; })
76 | .join(',\n');
77 | const edgesJson = this.edges
78 | .map(edge => { return edge.toJsonString(); })
79 | .join(',\n');
80 |
81 | try {
82 | const jsContent = this.generateJavascriptContent(nodesJson, rootNodesJson, edgesJson);
83 | this.fsUtils.writeFile(
84 | this.extensionContext?.asAbsolutePath(path.join('.', this.showComponentHierarchyJsFilename)),
85 | jsContent,
86 | callback
87 | );
88 | } catch (ex) {
89 | console.log('Angular Tools Exception:' + ex);
90 | }
91 | }
92 |
93 | private addNodesAndEdges(componentDict: { [selector: string]: Component; }, appendNodes: (nodeList: Node[]) => void, appendEdges: (edgeList: Edge[]) => void) {
94 | for (let selector in componentDict) {
95 | const component = componentDict[selector];
96 | if (component.isRoot) {
97 | this.generateDirectedGraphNodes(component.subComponents, component, true, '', appendNodes);
98 | this.generateDirectedGraphEdges(componentDict, component.subComponents, component, "", appendEdges);
99 | }
100 | }
101 | }
102 |
103 | private generateDirectedGraphNodes(components: Component[], component: Component, isRoot: boolean, parentSelector: string, appendNodes: (nodeList: Node[]) => void) {
104 | let componentFilename = component.filename.replace(this.workspaceFolder, '.');
105 | componentFilename = componentFilename.split('\\').join('/');
106 | const componentPosition = this.graphState.nodePositions[component.selector];
107 | appendNodes([new Node(component.selector, component.selector, componentFilename, component.filename, isRoot, isRoot ? NodeType.rootNode : NodeType.component, componentPosition)]);
108 | if (components.length > 0) {
109 | components.forEach((subComponent) => {
110 | if (parentSelector !== subComponent.selector) {
111 | this.generateDirectedGraphNodes(subComponent.subComponents, subComponent, subComponent.isRoot, component.selector, appendNodes);
112 | }
113 | });
114 | }
115 | }
116 |
117 | private generateDirectedGraphEdges(componentDict: { [selector: string]: Component; }, subComponents: Component[], currentComponent: Component, parentSelector: string, appendEdges: (edgeList: Edge[]) => void) {
118 | if (parentSelector.length > 0) {
119 | const id = this.edges.length;
120 | appendEdges([new Edge(id.toString(), parentSelector, currentComponent.selector, ArrowType.uses)]);
121 | }
122 | if (currentComponent.componentsRoutingToThis !== undefined && currentComponent.componentsRoutingToThis.length > 0) {
123 | currentComponent.componentsRoutingToThis.forEach(componentRoutingToThis => {
124 | const id = this.edges.length;
125 | appendEdges([new Edge(id.toString(), componentRoutingToThis.selector, currentComponent.selector, ArrowType.route)]);
126 | });
127 | }
128 | if (subComponents.length > 0 && currentComponent.selector !== parentSelector) {
129 | subComponents.forEach((subComponent) => {
130 | this.generateDirectedGraphEdges(componentDict, subComponent.subComponents, subComponent, currentComponent.selector, appendEdges);
131 | });
132 | }
133 | }
134 |
135 | private generateJavascriptContent(nodesJson: string, rootNodesJson: string, edgesJson: string): string {
136 | let template = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('templates', this.templateJsFilename)), 'utf8');
137 | let jsContent = template.replace('const nodes = new vis.DataSet([]);', `var nodes = new vis.DataSet([${nodesJson}]);`);
138 | jsContent = jsContent.replace('const rootNodes = [];', `var rootNodes = [${rootNodesJson}];`);
139 | jsContent = jsContent.replace('const edges = new vis.DataSet([]);', `var edges = new vis.DataSet([${edgesJson}]);`);
140 | jsContent = jsContent.replace('background: "#00FF00" // rootNode background color', `background: "${this.config.rootNodeBackgroundColor}" // rootNode background color`);
141 | jsContent = jsContent.replace('shape: \'box\' // The shape of the nodes.', `shape: '${this.config.rootNodeShape}'// The shape of the nodes.`);
142 | jsContent = jsContent.replace('type: "triangle" // edge arrow to type', `type: "${this.config.componentHierarchyEdgeArrowToType}" // edge arrow to type}`);
143 | jsContent = jsContent.replace('ctx.strokeStyle = \'blue\'; // graph selection guideline color', `ctx.strokeStyle = '${this.config.graphSelectionGuidelineColor}'; // graph selection guideline color`);
144 | jsContent = jsContent.replace('ctx.lineWidth = 1; // graph selection guideline width', `ctx.lineWidth = ${this.config.graphSelectionGuidelineWidth}; // graph selection guideline width`);
145 | jsContent = jsContent.replace('selectionCanvasContext.strokeStyle = \'red\';', `selectionCanvasContext.strokeStyle = '${this.config.graphSelectionColor}';`);
146 | jsContent = jsContent.replace('selectionCanvasContext.lineWidth = 2;', `selectionCanvasContext.lineWidth = ${this.config.graphSelectionWidth};`);
147 | jsContent = jsContent.replace('let showHierarchicalOptionsCheckboxChecked = false;', `let showHierarchicalOptionsCheckboxChecked = ${this.graphState.showHierarchicalOptions};`);
148 | jsContent = jsContent.replace('let hierarchicalOptionsDirectionSelectValue = undefined;', `let hierarchicalOptionsDirectionSelectValue = '${this.graphState.graphDirection}';`);
149 | jsContent = jsContent.replace('let hierarchicalOptionsSortMethodSelectValue = undefined;', `let hierarchicalOptionsSortMethodSelectValue = '${this.graphState.graphLayout}';`);
150 | jsContent = this.setGraphState(jsContent);
151 | return jsContent;
152 | }
153 | }
--------------------------------------------------------------------------------
/src/commands/showHierarchyBase.ts:
--------------------------------------------------------------------------------
1 | import { CommandBase } from '@commands';
2 | import { Config, DgmlManager, FileSystemUtils, GraphVizManager } from '@src';
3 | import { Edge, GraphState, Node } from '@model';
4 | import { Base64 } from 'js-base64';
5 | import * as fs from 'fs';
6 | import * as path from 'path';
7 | import * as vscode from 'vscode';
8 | import * as xmldom from '@xmldom/xmldom';
9 |
10 | const prettifyXml = require('prettify-xml');
11 | const xmlSerializer = require('xmlserializer');
12 |
13 | export class ShowHierarchyBase extends CommandBase {
14 | protected fsUtils: FileSystemUtils = new FileSystemUtils();
15 | protected config = new Config();
16 | protected extensionContext: vscode.ExtensionContext;
17 | protected graphState: GraphState;
18 | protected setNewState: (newGraphState: GraphState) => any;
19 | protected nodes: Node[] = [];
20 | protected edges: Edge[] = [];
21 | protected templateJsFilename: string = 'showHierarchy_Template.js';
22 | protected templateHtmlFilename: string = 'showHierarchy_Template.html';
23 | protected showComponentHierarchyJsFilename: string = 'showComponentHierarchy.js';
24 | protected showModuleHierarchyJsFilename: string = 'showModuleHierarchy.js';
25 | protected showHierarchyCssFilename: string = 'showHierarchy.css';
26 | protected fontAwesomeCssFilename: string = 'all.min.css';
27 | protected fontAwesomeFontFilename: string = '../webfonts/fa-';
28 |
29 | protected workspaceDirectory = this.fsUtils.getWorkspaceFolder();
30 |
31 | constructor(context: vscode.ExtensionContext, graphState: GraphState, setNewState: (newGraphState: GraphState) => any) {
32 | super();
33 | this.extensionContext = context;
34 | this.setNewState = setNewState;
35 | this.graphState = graphState;
36 | }
37 | protected appendNodes = (nodeList: Node[]) => {
38 | nodeList.forEach(newNode => {
39 | newNode.showPopupsOverNodesAndEdges = this.config.showPopupsOverNodesAndEdges;
40 | if (!this.nodes.some(node => node.id === newNode.id)) {
41 | this.nodes.push(newNode);
42 | }
43 | else {
44 | const existingNode = this.nodes.find(node => node.id === newNode.id);
45 | if (existingNode && (!existingNode.tsFilename || existingNode.tsFilename?.length === 0) && newNode.tsFilename && newNode.tsFilename.length > 0) {
46 | existingNode.tsFilename = newNode.tsFilename;
47 | existingNode.filename = newNode.filename;
48 | }
49 | }
50 | });
51 | };
52 | protected appendEdges = (edgeList: Edge[]) => {
53 | edgeList.forEach(newEdge => {
54 | newEdge.showPopupsOverNodesAndEdges = this.config.showPopupsOverNodesAndEdges;
55 | const mutualEdges = this.edges.filter(edge => edge.target === newEdge.source && edge. source=== newEdge.target);
56 | if (mutualEdges.length > 0) {
57 | newEdge.mutualEdgeCount += 1;
58 | mutualEdges.forEach(e => e.mutualEdgeCount += 1 );
59 | }
60 | if (!this.edges.some(edge => edge.source === newEdge.source && edge.target === newEdge.target)) {
61 | this.edges.push(newEdge);
62 | }
63 | });
64 | };
65 |
66 | protected getNonce() {
67 | let text = '';
68 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
69 | for (let i = 0; i < 32; i++) {
70 | text += possible.charAt(Math.floor(Math.random() * possible.length));
71 | }
72 | return text;
73 | }
74 |
75 | protected saveAsPng(pngFilename: string, messageText: string) {
76 | const dataUrl = messageText.split(',');
77 | if (dataUrl.length > 0) {
78 | const u8arr = Base64.toUint8Array(dataUrl[1]);
79 |
80 | const newFilePath = path.join(this.workspaceDirectory, pngFilename);
81 | this.fsUtils.writeFile(newFilePath, u8arr, () => { });
82 |
83 | vscode.window.setStatusBarMessage(`The file ${pngFilename} has been created in the root of the workspace.`);
84 | }
85 | }
86 |
87 | protected saveAsDgml(dgmlGraphFilename: string, messageText: string, popMessageText: string) {
88 | const message = JSON.parse(messageText);
89 | const direction = message.direction;
90 | const domImpl = new xmldom.DOMImplementation();
91 | const dgmlManager = new DgmlManager();
92 | const xmlDocument = dgmlManager.createNewDirectedGraph(domImpl, this.fixGraphDirection(direction), "Sugiyama", "-1");
93 | dgmlManager.addNodesAndLinks(xmlDocument, this.nodes, message.nodes, this.edges);
94 | // Serialize the xml into a string
95 | const xmlAsString = xmlSerializer.serializeToString(xmlDocument.documentElement);
96 | let fileContent = prettifyXml(xmlAsString);
97 | const xmlProlog = '\n';
98 | fileContent = xmlProlog + fileContent.replace('HasCategory('RootComponent')', "HasCategory('RootComponent')");
99 |
100 | // Write the prettified xml string to the ReadMe-ProjectStructure.dgml file.
101 | var directoryPath: string = this.fsUtils.getWorkspaceFolder();
102 | this.fsUtils.writeFile(path.join(directoryPath, dgmlGraphFilename), fileContent, () => {
103 | vscode.window.setStatusBarMessage(popMessageText, 10000);
104 | });
105 | }
106 |
107 | private fixGraphDirection(direction: string): string {
108 | let fixedDirection: string;
109 | switch (direction) {
110 | case 'UD':
111 | fixedDirection = 'TopToBottom';
112 | break;
113 | case 'DU':
114 | fixedDirection = 'BottomToTop';
115 | break;
116 | case 'LR':
117 | fixedDirection = 'LeftToRight';
118 | break;
119 | case 'RL':
120 | fixedDirection = 'RightToLeft';
121 | break;
122 | default:
123 | fixedDirection = '';
124 | break;
125 | }
126 | return fixedDirection;
127 | }
128 |
129 | protected saveAsDot(graphVizFilename: string, messageText: string, graphType: string, popMessageText: string) {
130 | const message = JSON.parse(messageText);
131 | const graphVizManager = new GraphVizManager();
132 | const fileContent = graphVizManager.createGraphVizDiagram(graphType, this.nodes, message.nodes, this.edges);
133 |
134 | // Write the prettified xml string to the ReadMe-ProjectStructure.dgml file.
135 | var directoryPath: string = this.fsUtils.getWorkspaceFolder();
136 | this.fsUtils.writeFile(path.join(directoryPath, graphVizFilename), fileContent, () => {
137 | vscode.window.setStatusBarMessage(popMessageText, 10000);
138 | });
139 | }
140 |
141 | protected setGraphState(jsContent: string): string {
142 | if (this.graphState.networkSeed !== undefined) {
143 | jsContent = jsContent.replace('var seed = network.getSeed();', `var seed = '${this.graphState.networkSeed}';`);
144 | }
145 | return jsContent;
146 | }
147 |
148 | protected generateHtmlContent(webview: vscode.Webview, outputJsFilename: string): string {
149 | let htmlContent = fs.readFileSync(this.extensionContext?.asAbsolutePath(path.join('templates', this.templateHtmlFilename)), 'utf8');
150 |
151 | const visPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'javascript', 'vis-network.min.js');
152 | const visUri = webview.asWebviewUri(visPath);
153 | htmlContent = htmlContent.replace('vis-network.min.js', visUri.toString());
154 |
155 | let cssPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'stylesheet', this.showHierarchyCssFilename);
156 | let cssUri = webview.asWebviewUri(cssPath);
157 | htmlContent = htmlContent.replace(this.showHierarchyCssFilename, cssUri.toString());
158 |
159 | const vscodyfiedFontAwesomeCssFilename = this.fixFontAwesomeFontUri(webview);
160 | cssPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'stylesheet', vscodyfiedFontAwesomeCssFilename);
161 | cssUri = webview.asWebviewUri(cssPath);
162 | htmlContent = htmlContent.replace(this.fontAwesomeCssFilename, cssUri.toString());
163 |
164 | const visJsMinCss = 'vis-network.min.css';
165 | const visCssPath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'stylesheet', visJsMinCss);
166 | const visCssUri = webview.asWebviewUri(visCssPath);
167 | htmlContent = htmlContent.replace(visJsMinCss, visCssUri.toString());
168 |
169 | const nonce = this.getNonce();
170 | htmlContent = htmlContent.replace('nonce-nonce', `nonce-${nonce}`);
171 | htmlContent = htmlContent.replace(/
13 |
14 |
15 |
16 |
17 |
46 |
47 |
48 |
49 |
50 |
51 |