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 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
+ }