Skip to content

Commit

Permalink
Add dependency graph to the README + script to gen (#976)
Browse files Browse the repository at this point in the history
Use `yarn workspaces list` to grab all workspaces and their dependencies
to each other, then use Graphviz to render a graph.
  • Loading branch information
mcmire authored Nov 18, 2022
1 parent 7ba4aff commit 3f2278d
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ module.exports = {
],
},
},
{
files: ['scripts/*.ts'],
rules: {
// All scripts will have shebangs.
'node/shebang': 'off',
},
},
],
rules: {
// Left disabled because various properties throughough this repo are snake_case because the
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ This is a monorepo that houses the following packages. Please refer to the READM
- [`@metamask/subject-metadata-controller`](packages/subject-metadata-controller)
- [`@metamask/transaction-controller`](packages/transaction-controller)

Here is a graph that shows the dependencies among all packages:

![Dependency graph](assets/dependency-graph.png)

> **Note**
> To regenerate this graph, run `yarn generate-dependency-graph`.
## Contributing

### Setup
Expand Down
Binary file added assets/dependency-graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions constraints.pro
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@ gen_enforced_field(WorkspaceCwd, 'types', './dist/index.d.ts') :-
gen_enforced_field(WorkspaceCwd, 'types', null) :-
workspace_field(WorkspaceCwd, 'private', true).

% "files" must be ["dist/"] for workspace packages and unset for the root.
% "files" must be ["dist/"] for workspace packages and [] for the root.
gen_enforced_field(WorkspaceCwd, 'files', ['dist/']) :-
\+ workspace_field(WorkspaceCwd, 'private', true).
gen_enforced_field(WorkspaceCwd, 'files', null) :-
gen_enforced_field(WorkspaceCwd, 'files', []) :-
workspace_field(WorkspaceCwd, 'private', true).

% All workspace packages must have the same "build:docs" script.
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"type": "git",
"url": "https://github.com/MetaMask/controllers.git"
},
"files": [],
"workspaces": [
"packages/*"
],
Expand All @@ -16,6 +17,7 @@
"build:docs": "yarn workspaces foreach --parallel --interlaced --verbose run build:docs",
"build:watch": "yarn run build --watch",
"changelog:validate": "yarn workspaces foreach --parallel --interlaced --verbose run changelog:validate",
"generate-dependency-graph": "ts-node scripts/generate-dependency-graph.ts",
"lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints",
"lint:eslint": "eslint . --cache --ext js,ts",
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write && yarn constraints --fix",
Expand All @@ -36,6 +38,7 @@
"@metamask/eslint-config-jest": "^9.0.0",
"@metamask/eslint-config-nodejs": "^9.0.0",
"@metamask/eslint-config-typescript": "^9.0.1",
"@types/node": "^14.14.31",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.24.0",
Expand All @@ -46,12 +49,15 @@
"eslint-plugin-jsdoc": "^36.1.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.1",
"execa": "^5.0.0",
"isomorphic-fetch": "^3.0.0",
"prettier": "^2.6.2",
"prettier-plugin-packagejson": "^2.2.17",
"rimraf": "^3.0.2",
"simple-git-hooks": "^2.8.0",
"typescript": "~4.6.3"
"ts-node": "^10.9.1",
"typescript": "~4.6.3",
"which": "^3.0.0"
},
"packageManager": "[email protected]",
"engines": {
Expand Down
126 changes: 126 additions & 0 deletions scripts/generate-dependency-graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!yarn ts-node

import fs from 'fs';
import os from 'os';
import path from 'path';
import execa from 'execa';
import which from 'which';

/**
* Retrieves the path to `dot`, which is one of the tools within the Graphviz
* toolkit to render a graph.
*
* @returns The path if `dot` exists or else null.
*/
async function getDotExecutablePath() {
try {
return await which('dot');
} catch (error) {
if (error.message === 'dot not found') {
return null;
}
throw error;
}
}

/**
* Uses `yarn workspaces list` to retrieve all of the workspace packages in this
* repo and their relationship to each other, produces code that can be
* passed to the `dot` tool, and writes that to a file.
*
* @param dotFilePath - The path to the file that will be written and ultimately
* passed to `dot`.
*/
async function generateGraphDotFile(dotFilePath) {
const { stdout } = await execa('yarn', [
'workspaces',
'list',
'--json',
'--verbose',
]);

const modules = stdout
.split('\n')
.map((line) => JSON.parse(line))
.slice(1);

const nodes = modules.map((mod) => {
const fullPackageName = mod.name;
const shortPackageName = fullPackageName
.replace(/^@metamask\//u, '')
.replace(/-/gu, '_');
return ` ${shortPackageName} [label="${fullPackageName}"];`;
});

const connections = [];
modules.forEach((mod) => {
const fullPackageName = mod.name;
const shortPackageName = fullPackageName
.replace(/^@metamask\//u, '')
.replace(/-/gu, '_');
mod.workspaceDependencies.forEach((dependency) => {
const shortDependencyName = dependency
.replace(/^packages\//u, '')
.replace(/-/gu, '_');
connections.push(` ${shortPackageName} -> ${shortDependencyName};`);
});
});

const graphSource = [
'digraph G {',
' rankdir="LR";',
...nodes,
...connections,
'}',
].join('\n');

await fs.promises.writeFile(dotFilePath, graphSource);
}

/**
* Uses `dot` to render the dependency graph.
*
* @param dotExecutablePath - The path to `dot`.
* @param dotFilePath - The path to file that instructs `dot` how to render the
* graph.
* @param graphFilePath - The path to the image file that will be written.
*/
async function renderGraph(dotExecutablePath, dotFilePath, graphFilePath) {
await execa(dotExecutablePath, [
dotFilePath,
'-T',
'png',
'-o',
graphFilePath,
]);
}

/**
* The entrypoint to this script.
*/
async function main() {
const tempDirectory = await fs.promises.mkdtemp(
path.join(os.tmpdir(), 'controllers-'),
);
const dotFilePath = path.join(tempDirectory, 'dependency-graph.dot');
const graphFilePath = path.resolve(
__dirname,
'../assets/dependency-graph.png',
);
const dotExecutablePath = await getDotExecutablePath();

if (dotExecutablePath) {
await generateGraphDotFile(dotFilePath);
await renderGraph(dotExecutablePath, dotFilePath, graphFilePath);
console.log(`Done! Graph written to ${graphFilePath}.`);
} else {
throw new Error(
"It looks like you don't have Graphviz installed. You'll need to install this to generate the dependency graph.",
);
}
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Loading

0 comments on commit 3f2278d

Please sign in to comment.