Skip to content

Commit

Permalink
build: Include wasm-graphviz
Browse files Browse the repository at this point in the history
  • Loading branch information
seantiz committed Nov 14, 2024
1 parent a7d8919 commit 3aa5d0c
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 63 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
# Changelog

## 0.8.4 (2024-11-12)

* We now bundle hpcc-js/wasm-graphviz into Dryfold just to make it simpler. No more need for a separate install of Graphviz.

* Dryfold's Feature Report now embeds the generated SVG from generateDataViz().

* Removed the task that generates PNGs because the format isn't supported by wasm-graphviz' Format type
and the more robust SVG paths included in the Feature Report make generating PNGs redundant

## 0.8.2 (2024-11-12)

* Removed tree-sitter-typescript because that feature idea has been put on the shelf for now.

* Removed node-graphviz until we can decide what's the most well-maintained option when bundling graphviz.

## 0.8.1 (2024-11-12)
Expand Down
24 changes: 2 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,9 @@ But beyond that, Dryfold helps with:
3. Export to Github Projects - plan the work among the team. Everyone loves a kanban, right? I do.
4. Export to CSV if you want to view the source codebase in a dataviz app like Gephi.
5. Export to JSON for other project management/kanban board apps like Kanri.
6. Export to TSV, SVG, PNG and .dot.
6. Export to TSV, SVG and .dot.

## Install Graphviz Before Running

You need to have `graphviz` installed globally on your local machine for the reports to finish compiling without error. The CSV, SVG, PNG and .dot file generation all rely on `graphviz`. I found that CSV spreadsheets were easier to write (with the nodes and edge relationships intact) when first reading from the .dot file rather than trying to write CSVs directly from the module map.

I'll look at bundling graphviz into the app to remove this requirement soon.

For now, to install Graphviz locally:

### MacOS

```bash
brew install graphviz
```
### Windows

The recommended way is through Chocolatey:

```shell
choco install graphviz
```
## Optional: Install Github's gh cli tool
## Install Github's gh Before Creating Github Projects

If you want to publish a new Github project from your Dryfold maps, you'll need to have `gh` CLI tool installed locally so you can post to your Github profile. Your `gh` will also need permission to create and edit projects on your Github account.

Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dryfold-cli",
"version": "0.8.3",
"version": "0.8.4",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -22,5 +22,8 @@
"tsx": "^4.19.2",
"typescript": "^5.6.3"
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"dependencies": {
"@hpcc-js/wasm-graphviz": "^1.6.1"
}
}
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/export/complexityReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@ export function printComplexityReport(moduleMap: Map<string, ComplexityValues>)
if(!fs.existsSync('./allreports'))
fs.mkdirSync('./allreports', { recursive: true} )

fs.writeFileSync('./allreports/tasks_complexity_report.html', html);
fs.writeFileSync('./allreports/complexity_report.html', html);
console.log(`Report generated: ${path.resolve('./allreports/complexity_report.html')}`);
}
98 changes: 61 additions & 37 deletions src/export/core.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import fs from 'fs'
import path from 'path'
import type { DesignValues } from "../schema";
import { promisify } from 'util';
import { exec as execCallback } from 'child_process';

const exec = promisify(execCallback);
import type { DesignValues, LayerType } from "../schema";
import { Graphviz } from '@hpcc-js/wasm-graphviz';

interface N {
node: string;
Expand All @@ -18,6 +15,8 @@ interface E {
target: string;
}

const graphviz = await Graphviz.load()

async function dotToCSV(dotFilePath: string): Promise<void> {
try {
const dotContent = await fs.promises.readFile(dotFilePath, 'utf-8');
Expand All @@ -30,7 +29,7 @@ async function dotToCSV(dotFilePath: string): Promise<void> {
}
});

const { stdout } = await exec(`dot -Tplain "${dotFilePath}"`);
const plaintext = graphviz.layout(dotContent, "plain", "dot");

const nodes: N[] = [];
const edges: E[] = [];
Expand All @@ -43,7 +42,7 @@ async function dotToCSV(dotFilePath: string): Promise<void> {
'unknown': 0
};

stdout.split('\n').forEach(line => {
plaintext.split('\n').forEach(line => {
const parts = line.trim().split(' ');
if (parts[0] === 'node') {
const nodeName = parts[1].replace(/"/g, '');
Expand Down Expand Up @@ -92,11 +91,9 @@ export async function generateDataViz(moduleMap: Map<string, DesignValues>) {
const dotFilePath = './allreports/dependencygraph.dot';
await fs.promises.writeFile(dotFilePath, dot);

await Promise.all([
exec(`dot -Tsvg ${dotFilePath} -o ./allreports/dependencies.svg`),
exec(`dot -Tpng ${dotFilePath} -o ./allreports/dependencies.png`),
dotToCSV(dotFilePath)
]);
const svg = graphviz.layout(dot, "svg", "fdp");
await fs.promises.writeFile('./allreports/dependencies.svg', svg);
dotToCSV(dotFilePath)

console.log('Successfully generated graph and CSV files');
} catch (error) {
Expand All @@ -107,43 +104,73 @@ export async function generateDataViz(moduleMap: Map<string, DesignValues>) {

export function createDot(moduleMap: Map<string, DesignValues>) {
let dot = 'digraph Dependencies {\n';
dot += ' graph [rankdir=TB, splines=ortho, nodesep=0.8, ranksep=2.0];\n';
dot += ' node [shape=box];\n';

const layers: Record<LayerType, Set<string>> = {
core: new Set<string>(),
interface: new Set<string>(),
derived: new Set<string>(),
utility: new Set<string>()
};
const unknownLayers = new Set<string>();
const knownNodes = new Set<string>();
const cleanModuleName = (name: string): string => path.basename(name).replace('.h', '');

for (const [file, data] of moduleMap) {
const nodeName = path.basename(file);
const className = nodeName.replace('.h', '');

const layer = (() => {
// First try file-based layer type if available
if (data.fileLayerType) {
return data.fileLayerType;
}

// Fall back to module relationships-based layer type
const relationships = data.moduleRelationships;
if (!relationships || !relationships[className]) {
unknownLayers.add(nodeName);
return 'unknown';
}
return relationships[className].type || 'unknown';
})();
if (data.fileLayerType) {
layers[data.fileLayerType].add(nodeName);
knownNodes.add(nodeName);
}

if (data.moduleRelationships) {
Object.entries(data.moduleRelationships).forEach(([moduleName, moduleData]) => {
if (moduleData.type) {
const cleanName = cleanModuleName(moduleName);
layers[moduleData.type].add(cleanName);
knownNodes.add(cleanName);
}
});
}

if (layer !== 'unknown') {
knownNodes.add(nodeName);
dot += ` "${nodeName}" [label="${nodeName}", layer="${layer}"];\n`;
if (!knownNodes.has(nodeName)) {
unknownLayers.add(nodeName);
}
}

// Only add edges between known nodes
// Nodes
const colors = {
core: 'lightgrey',
interface: 'lightblue',
derived: 'lightgreen',
utility: 'lightyellow'
};

Object.entries(layers).forEach(([layer, nodes]) => {
if (nodes.size > 0) {
dot += ` subgraph cluster_${layer} {\n`;
dot += ' rank = same;\n';
dot += ` label = "${layer.charAt(0).toUpperCase() + layer.slice(1)} Layer";\n`;
dot += ' style = filled;\n';
dot += ` color = ${colors[layer as LayerType]};\n`;
dot += ' node [style=filled,color=white];\n';

nodes.forEach(nodeName => {
dot += ` "${nodeName}" [label="${nodeName}", layer="${layer}"];\n`;
});

dot += ' }\n';
}
});

// Edges
for (const [file, data] of moduleMap) {
const sourceNode = path.basename(file);
if (!knownNodes.has(sourceNode) || !data.includes) continue;

data.includes.forEach((include) => {
data.includes.forEach(include => {
const includeName = path.basename(include.replace(/#include\s*[<"]([^>"]+)[>"]/g, '$1'));
if (knownNodes.has(includeName)) {
dot += ` "${sourceNode}" -> "${includeName}";\n`;
Expand All @@ -154,15 +181,12 @@ export function createDot(moduleMap: Map<string, DesignValues>) {
if (unknownLayers.size > 0) {
dot += '\n /* Modules with unknown layers:\n';
Array.from(unknownLayers).sort().forEach(name => {
dot += ` * ${name.replace('.h', '')}\n`;
dot += ` * ${name}\n`;
});
dot += ' */\n';
}

dot += '}';

return {
dot,
unknownLayers: Array.from(unknownLayers)
};
return { dot, unknownLayers: Array.from(unknownLayers) };
}
11 changes: 11 additions & 0 deletions src/export/featureReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export function printFeatureReport(moduleMap: Map<string, DesignValues>) {
const { unknownLayers } = createDot(moduleMap);
const styling = '../src/export/styles/features.css';

let svg = '';
const svgFromData = './allreports/dependencies.svg';
if (fs.existsSync(svgFromData)) {
svg = fs.readFileSync(svgFromData, 'utf-8');
} else {
svg = "No renderable content"
}

const unknownLayersSection = unknownLayers.length ? `
<div class="warning-section">
<h2>⚠️ Unclassified Modules</h2>
Expand All @@ -32,6 +40,9 @@ export function printFeatureReport(moduleMap: Map<string, DesignValues>) {
${unknownLayersSection}
<div class="architecture-overview">
<h2>System Architecture Overview</h2>
<div class="dependency-graph">
${svg}
</div>
<div class="layer-breakdown">
${generateLayerSummary(moduleMap)}
</div>
Expand Down
15 changes: 15 additions & 0 deletions src/export/styles/features.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ body {
margin-right: 8px;
}

.dependency-graph svg {
max-width: 100%;
height: auto;
margin: 20px 0;
}

/* Dark mode */
@media (prefers-color-scheme: dark) {
body {
Expand Down Expand Up @@ -120,4 +126,13 @@ body {
.incoming strong {
color: #cccccc;
}

.dependency-graph svg {
filter: invert(1) hue-rotate(180deg);
background: transparent;
}

.dependency-graph svg text {
filter: invert(1) hue-rotate(180deg);
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ async function main() {

const codebaseDesign = findDesign(codebaseComplexity)
// AST removed from the map from here on
printFeatureReport(codebaseDesign)
await generateDataViz(codebaseDesign)
printFeatureReport(codebaseDesign)
generateGHProject(codebaseDesign)

const postGH = (await terminal.question('\n\nWould you like to create a GitHub project for these tasks? (y/n): ')).trim().toLowerCase()
Expand Down

0 comments on commit 3aa5d0c

Please sign in to comment.