nx-visualizer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/cli.js +94 -0
- package/package.json +26 -0
- package/src/cli.ts +161 -0
- package/tsconfig.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# nx-visualizer
|
|
2
|
+
|
|
3
|
+
CLI that reads an Nx project graph and reports **circular dependencies**, **orphan projects** (nothing depends on them), and **layer violations** against a simple apps → features → shared → core model.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Node.js** (current LTS is fine)
|
|
8
|
+
- An **Nx workspace** where you can export the dependency graph
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
From this repository:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
npm run build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Compiled output is written to `dist/`.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
Run everything from the **root of your Nx workspace** (the same folder where `nx.json` lives).
|
|
24
|
+
|
|
25
|
+
### 1. Export the graph
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx nx graph --file=graph.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This creates `graph.json` in the current directory. The analyzer expects that exact filename in the working directory.
|
|
32
|
+
|
|
33
|
+
### 2. Analyze
|
|
34
|
+
|
|
35
|
+
Run the built CLI directly:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx nx-visualizer analyze
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What you get
|
|
42
|
+
|
|
43
|
+
| Check | Meaning |
|
|
44
|
+
|--------|--------|
|
|
45
|
+
| **Circular dependencies** | Projects involved in dependency cycles (first node hit when a cycle is detected). |
|
|
46
|
+
| **Orphan libraries** | Projects that never appear as a dependency target—often unused entry points or mis-wired libs. |
|
|
47
|
+
| **Layer violations** | Edges that go “up” the stack (e.g. a lower layer depending on a higher one). |
|
|
48
|
+
|
|
49
|
+
### Default layers
|
|
50
|
+
|
|
51
|
+
Layers are inferred from project names/paths using these prefixes:
|
|
52
|
+
|
|
53
|
+
| Layer | Path hints |
|
|
54
|
+
|--------|------------|
|
|
55
|
+
| `apps` | `apps/` |
|
|
56
|
+
| `features` | `libs/features/` |
|
|
57
|
+
| `shared` | `libs/shared/` |
|
|
58
|
+
| `core` | `libs/core/` |
|
|
59
|
+
|
|
60
|
+
Anything else is treated as `unknown` for layer rules.
|
|
61
|
+
|
|
62
|
+
Allowed direction is **apps → features → shared → core** (higher index must not depend on lower index in that list). Violations are listed as `source → target`.
|
|
63
|
+
|
|
64
|
+
## Customizing layers
|
|
65
|
+
|
|
66
|
+
The prefix map lives in `src/cli.ts` as `DEFAULT_LAYERS`. Adjust it to match your repo’s folder layout, then rebuild (`npm run build`).
|
|
67
|
+
|
|
68
|
+
## Troubleshooting
|
|
69
|
+
|
|
70
|
+
- **`graph.json not found`** — Run `nx graph --file=graph.json` from the Nx workspace root, or run the analyzer with that file present in the current working directory.
|
|
71
|
+
- **Empty or surprising results** — Confirm `graph.json` is fresh and that you are analyzing the same workspace you exported.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
ISC
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.name("nx-visualizer")
|
|
8
|
+
.description("Nx visualizer")
|
|
9
|
+
.version("1.0.0");
|
|
10
|
+
const DEFAULT_LAYERS = {
|
|
11
|
+
apps: ["apps/"],
|
|
12
|
+
features: ["libs/features/"],
|
|
13
|
+
shared: ["libs/shared/"],
|
|
14
|
+
core: ["libs/core/"],
|
|
15
|
+
};
|
|
16
|
+
function detectLayer(projectName) {
|
|
17
|
+
for (const [layer, prefixes] of Object.entries(DEFAULT_LAYERS)) {
|
|
18
|
+
for (const prefix of prefixes) {
|
|
19
|
+
if (projectName.includes(prefix)) {
|
|
20
|
+
return layer;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return "unknown";
|
|
25
|
+
}
|
|
26
|
+
function loadGraph() {
|
|
27
|
+
const file = "graph.json";
|
|
28
|
+
if (!fs.existsSync(file)) {
|
|
29
|
+
console.log(chalk.red("graph.json not found"));
|
|
30
|
+
console.log("Run: nx graph --file=graph.json");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
34
|
+
}
|
|
35
|
+
function detectCircularDeps(graph) {
|
|
36
|
+
const visited = new Set();
|
|
37
|
+
const stack = new Set();
|
|
38
|
+
const cycles = [];
|
|
39
|
+
function dfs(node) {
|
|
40
|
+
if (stack.has(node)) {
|
|
41
|
+
cycles.push(node);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (visited.has(node))
|
|
45
|
+
return;
|
|
46
|
+
visited.add(node);
|
|
47
|
+
stack.add(node);
|
|
48
|
+
const deps = graph.dependencies[node] || [];
|
|
49
|
+
for (const dep of deps) {
|
|
50
|
+
dfs(dep.target);
|
|
51
|
+
}
|
|
52
|
+
stack.delete(node);
|
|
53
|
+
}
|
|
54
|
+
Object.keys(graph.nodes).forEach(dfs);
|
|
55
|
+
return cycles;
|
|
56
|
+
}
|
|
57
|
+
function detectOrphans(graph) {
|
|
58
|
+
const inbound = new Set();
|
|
59
|
+
Object.values(graph.dependencies).forEach((deps) => {
|
|
60
|
+
deps.forEach((dep) => inbound.add(dep.target));
|
|
61
|
+
});
|
|
62
|
+
return Object.keys(graph.nodes).filter((node) => !inbound.has(node));
|
|
63
|
+
}
|
|
64
|
+
function detectLayerViolations(graph) {
|
|
65
|
+
const violations = [];
|
|
66
|
+
const layerOrder = ["apps", "features", "shared", "core"];
|
|
67
|
+
Object.entries(graph.dependencies).forEach(([source, deps]) => {
|
|
68
|
+
const sourceLayer = detectLayer(source);
|
|
69
|
+
deps.forEach(({ target }) => {
|
|
70
|
+
const targetLayer = detectLayer(target);
|
|
71
|
+
if (layerOrder.indexOf(sourceLayer) <
|
|
72
|
+
layerOrder.indexOf(targetLayer)) {
|
|
73
|
+
violations.push(`${source} → ${target}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
return violations;
|
|
78
|
+
}
|
|
79
|
+
program.command("analyze").action(() => {
|
|
80
|
+
const graph = loadGraph();
|
|
81
|
+
console.log(chalk.blue("\nAnalyzing Nx workspace architecture...\n"));
|
|
82
|
+
const circular = detectCircularDeps(graph);
|
|
83
|
+
const orphans = detectOrphans(graph);
|
|
84
|
+
const violations = detectLayerViolations(graph);
|
|
85
|
+
console.log(chalk.yellow(`Circular dependencies: ${circular.length}`));
|
|
86
|
+
circular.forEach((c) => console.log(chalk.gray(` - ${c}`)));
|
|
87
|
+
console.log(chalk.yellow(`\nOrphan libraries: ${orphans.length}`));
|
|
88
|
+
orphans.forEach((o) => console.log(chalk.gray(` - ${o}`)));
|
|
89
|
+
console.log(chalk.red(`\nLayer violations: ${violations.length}`));
|
|
90
|
+
violations.forEach((v) => console.log(chalk.gray(` - ${v}`)));
|
|
91
|
+
console.log(chalk.green("\nAnalysis complete.\n"));
|
|
92
|
+
});
|
|
93
|
+
program.parse(process.argv);
|
|
94
|
+
//# sourceMappingURL=cli.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nx-visualizer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "src/cli.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nx-visualizer": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [],
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"chalk": "^5.6.2",
|
|
19
|
+
"commander": "^14.0.3"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.10.0",
|
|
23
|
+
"ts-node": "^10.9.2",
|
|
24
|
+
"typescript": "^6.0.2"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name("nx-visualizer")
|
|
10
|
+
.description("Nx visualizer")
|
|
11
|
+
.version("1.0.0");
|
|
12
|
+
|
|
13
|
+
type Graph = {
|
|
14
|
+
nodes: Record<string, any>;
|
|
15
|
+
dependencies: Record<string, { target: string }[]>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DEFAULT_LAYERS = {
|
|
19
|
+
apps: ["apps/"],
|
|
20
|
+
features: ["libs/features/"],
|
|
21
|
+
shared: ["libs/shared/"],
|
|
22
|
+
core: ["libs/core/"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function detectLayer(projectName: string) {
|
|
26
|
+
for (const [layer, prefixes] of Object.entries(DEFAULT_LAYERS)) {
|
|
27
|
+
for (const prefix of prefixes) {
|
|
28
|
+
if (projectName.includes(prefix)) {
|
|
29
|
+
return layer;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return "unknown";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadGraph(): Graph {
|
|
37
|
+
const file = "graph.json";
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(file)) {
|
|
40
|
+
console.log(chalk.red("graph.json not found"));
|
|
41
|
+
console.log("Run: nx graph --file=graph.json");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectCircularDeps(graph: Graph) {
|
|
49
|
+
const visited = new Set<string>();
|
|
50
|
+
const stack = new Set<string>();
|
|
51
|
+
const cycles: string[] = [];
|
|
52
|
+
|
|
53
|
+
function dfs(node: string) {
|
|
54
|
+
if (stack.has(node)) {
|
|
55
|
+
cycles.push(node);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (visited.has(node)) return;
|
|
60
|
+
|
|
61
|
+
visited.add(node);
|
|
62
|
+
stack.add(node);
|
|
63
|
+
|
|
64
|
+
const deps = graph.dependencies[node] || [];
|
|
65
|
+
|
|
66
|
+
for (const dep of deps) {
|
|
67
|
+
dfs(dep.target);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stack.delete(node);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Object.keys(graph.nodes).forEach(dfs);
|
|
74
|
+
|
|
75
|
+
return cycles;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectOrphans(graph: Graph) {
|
|
79
|
+
const inbound = new Set<string>();
|
|
80
|
+
|
|
81
|
+
Object.values(graph.dependencies).forEach((deps) => {
|
|
82
|
+
deps.forEach((dep) => inbound.add(dep.target));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return Object.keys(graph.nodes).filter(
|
|
86
|
+
(node) => !inbound.has(node)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function detectLayerViolations(graph: Graph) {
|
|
91
|
+
const violations: string[] = [];
|
|
92
|
+
|
|
93
|
+
const layerOrder = ["apps", "features", "shared", "core"];
|
|
94
|
+
|
|
95
|
+
Object.entries(graph.dependencies).forEach(
|
|
96
|
+
([source, deps]) => {
|
|
97
|
+
const sourceLayer = detectLayer(source);
|
|
98
|
+
|
|
99
|
+
deps.forEach(({ target }) => {
|
|
100
|
+
const targetLayer = detectLayer(target);
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
layerOrder.indexOf(sourceLayer) <
|
|
104
|
+
layerOrder.indexOf(targetLayer)
|
|
105
|
+
) {
|
|
106
|
+
violations.push(
|
|
107
|
+
`${source} → ${target}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return violations;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
program.command("analyze").action(() => {
|
|
118
|
+
const graph = loadGraph();
|
|
119
|
+
|
|
120
|
+
console.log(
|
|
121
|
+
chalk.blue("\nAnalyzing Nx workspace architecture...\n")
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const circular = detectCircularDeps(graph);
|
|
125
|
+
const orphans = detectOrphans(graph);
|
|
126
|
+
const violations = detectLayerViolations(graph);
|
|
127
|
+
|
|
128
|
+
console.log(
|
|
129
|
+
chalk.yellow(
|
|
130
|
+
`Circular dependencies: ${circular.length}`
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
circular.forEach((c) =>
|
|
135
|
+
console.log(chalk.gray(` - ${c}`))
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
console.log(
|
|
139
|
+
chalk.yellow(`\nOrphan libraries: ${orphans.length}`)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
orphans.forEach((o) =>
|
|
143
|
+
console.log(chalk.gray(` - ${o}`))
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
console.log(
|
|
147
|
+
chalk.red(
|
|
148
|
+
`\nLayer violations: ${violations.length}`
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
violations.forEach((v) =>
|
|
153
|
+
console.log(chalk.gray(` - ${v}`))
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
console.log(
|
|
157
|
+
chalk.green("\nAnalysis complete.\n")
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
program.parse(process.argv);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
// File Layout
|
|
5
|
+
// "rootDir": "./src",
|
|
6
|
+
// "outDir": "./dist",
|
|
7
|
+
|
|
8
|
+
// Environment Settings
|
|
9
|
+
// See also https://aka.ms/tsconfig/module
|
|
10
|
+
"module": "nodenext",
|
|
11
|
+
"target": "esnext",
|
|
12
|
+
"types": ["node"],
|
|
13
|
+
// For nodejs:
|
|
14
|
+
// "lib": ["esnext"],
|
|
15
|
+
// "types": ["node"],
|
|
16
|
+
// and npm install -D @types/node
|
|
17
|
+
|
|
18
|
+
// Other Outputs
|
|
19
|
+
"sourceMap": true,
|
|
20
|
+
"declaration": true,
|
|
21
|
+
"declarationMap": true,
|
|
22
|
+
"rootDir": "src",
|
|
23
|
+
"outDir": "./dist",
|
|
24
|
+
|
|
25
|
+
// Stricter Typechecking Options
|
|
26
|
+
"noUncheckedIndexedAccess": true,
|
|
27
|
+
"exactOptionalPropertyTypes": true,
|
|
28
|
+
|
|
29
|
+
// Style Options
|
|
30
|
+
// "noImplicitReturns": true,
|
|
31
|
+
// "noImplicitOverride": true,
|
|
32
|
+
// "noUnusedLocals": true,
|
|
33
|
+
// "noUnusedParameters": true,
|
|
34
|
+
// "noFallthroughCasesInSwitch": true,
|
|
35
|
+
// "noPropertyAccessFromIndexSignature": true,
|
|
36
|
+
|
|
37
|
+
// Recommended Options
|
|
38
|
+
"strict": true,
|
|
39
|
+
"jsx": "react-jsx",
|
|
40
|
+
"verbatimModuleSyntax": true,
|
|
41
|
+
"isolatedModules": true,
|
|
42
|
+
"noUncheckedSideEffectImports": true,
|
|
43
|
+
"moduleDetection": "force",
|
|
44
|
+
"skipLibCheck": true,
|
|
45
|
+
}
|
|
46
|
+
}
|