react-dep-galaxy 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/LICENSE +21 -0
- package/README.md +129 -0
- package/README.zh-TW.md +130 -0
- package/analyze-components.mjs +261 -0
- package/bin/galaxy.js +189 -0
- package/package.json +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 [scyprodigy]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# 🌌 React Galaxy
|
|
2
|
+
|
|
3
|
+
> A React component dependency visualizer that turns your codebase into an interactive galaxy.
|
|
4
|
+
|
|
5
|
+
[🚀 Live Demo](https://scyprodigy.github.io/react-galaxy/)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🖼️ Preview
|
|
10
|
+
|
|
11
|
+
> Live demo: 12 components, node size represents line count, edges represent dependency direction.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- 🧠 **Inspired by LLM Neuroanatomy**
|
|
18
|
+
Visualize your component architecture as a neural network — spot shared components and dependency chains at a glance.
|
|
19
|
+
|
|
20
|
+
- 📐 **Orthogonal Metrics (lines vs. dependencies)**
|
|
21
|
+
Node size = line count; edge count = number of dependencies. Understand both scale and complexity in one view.
|
|
22
|
+
|
|
23
|
+
- ⚡ **Real-time Interaction**
|
|
24
|
+
Drag, zoom, hover to inspect component details (path, line count, dependency list), and click to trace dependency chains.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🚀 Quick Start
|
|
29
|
+
|
|
30
|
+
### Using npx (no install required)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx react-dep-galaxy scan ./src
|
|
34
|
+
npx react-dep-galaxy view
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Global install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g react-dep-galaxy
|
|
41
|
+
galaxy scan ./src
|
|
42
|
+
galaxy view
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Open your browser at `http://localhost:3000`
|
|
46
|
+
|
|
47
|
+
> 💡 Running `scan` generates a `galaxy.json` file. `view` starts a local server that reads it.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 📦 How It Works
|
|
52
|
+
|
|
53
|
+
### 1️⃣ Scan Phase
|
|
54
|
+
|
|
55
|
+
- Parses your React project (supports `.jsx`, `.tsx`, `.js`)
|
|
56
|
+
- Detects function components and class components
|
|
57
|
+
- Builds a component dependency graph from imports and JSX tag usage
|
|
58
|
+
- Outputs `galaxy.json`
|
|
59
|
+
|
|
60
|
+
### 2️⃣ View Phase
|
|
61
|
+
|
|
62
|
+
- Starts a local HTTP server
|
|
63
|
+
- Renders a force-directed graph using `vis-network`
|
|
64
|
+
- Node size and color reflect line count and dependency depth
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 📁 Output Example
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
[
|
|
72
|
+
{
|
|
73
|
+
"name": "App",
|
|
74
|
+
"path": "src/App.jsx",
|
|
75
|
+
"lines": 24,
|
|
76
|
+
"dependencies": ["Header", "MainContent", "Footer"]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 🛠️ Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/<your-username>/react-galaxy.git
|
|
87
|
+
cd react-galaxy
|
|
88
|
+
npm install
|
|
89
|
+
npm link # Links the `galaxy` command globally
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 🚀 Deploy to GitHub Pages
|
|
95
|
+
|
|
96
|
+
Static demo files are located in the `docs/` directory. To publish:
|
|
97
|
+
|
|
98
|
+
1. Go to your repo → **Settings → Pages**
|
|
99
|
+
2. Set Branch: `main`, Folder: `/docs`
|
|
100
|
+
3. Click **Save**
|
|
101
|
+
|
|
102
|
+
Your demo will be live at `https://[yourName].github.io/react-galaxy/` within a minute.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🤝 Contributing
|
|
107
|
+
|
|
108
|
+
All contributions are welcome 🙌
|
|
109
|
+
|
|
110
|
+
1. Fork the repo
|
|
111
|
+
2. Create a branch: `git checkout -b feature/amazing-feature`
|
|
112
|
+
3. Commit your changes: `git commit -m "feat: add amazing feature"`
|
|
113
|
+
4. Push the branch: `git push origin feature/amazing-feature`
|
|
114
|
+
5. Open a Pull Request
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🧭 Roadmap
|
|
119
|
+
|
|
120
|
+
- [ ] Precise AST parsing (TypeScript, HOC support)
|
|
121
|
+
- [ ] Plugin system (custom metrics and output formats)
|
|
122
|
+
- [ ] AI-assisted analysis (LLM-powered component complexity detection)
|
|
123
|
+
- [ ] Remote project analysis (upload code or link a GitHub repo)
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 📄 License
|
|
128
|
+
|
|
129
|
+
MIT License © 2026 [scyprodigy]
|
package/README.zh-TW.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# 🌌 React Galaxy
|
|
2
|
+
|
|
3
|
+
> A React component dependency visualizer that turns your codebase into an interactive galaxy.
|
|
4
|
+
> 一個將 React 元件依賴關係轉化為「星系圖」的可視化工具。
|
|
5
|
+
|
|
6
|
+
[🚀 Live Demo](https://scyprodigy.github.io/react-galaxy/)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 🖼️ Preview
|
|
11
|
+
|
|
12
|
+
> 實際展示:12 個元件的依賴關係,節點大小代表程式碼行數,連線表示依賴方向。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ✨ Features
|
|
17
|
+
|
|
18
|
+
- 🧠 **Inspired by LLM Neuroanatomy**
|
|
19
|
+
將元件架構視覺化為神經網絡般的結構,一眼看出共用元件與依賴鏈。
|
|
20
|
+
|
|
21
|
+
- 📐 **Orthogonal Metrics(行數 vs 依賴數)**
|
|
22
|
+
節點大小 = 程式碼行數;連線數 = 依賴數量。同時掌握規模與複雜度。
|
|
23
|
+
|
|
24
|
+
- ⚡ **Real-time Interaction**
|
|
25
|
+
拖曳、縮放;滑鼠懸停查看元件資訊(路徑、行數、依賴列表);點擊追蹤依賴關係。
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🚀 Quick Start
|
|
30
|
+
|
|
31
|
+
### 使用 npx(無需安裝)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx react-galaxy scan ./src
|
|
35
|
+
npx react-galaxy view
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 本地安裝後使用
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g react-galaxy
|
|
42
|
+
galaxy scan ./src
|
|
43
|
+
galaxy view
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
打開瀏覽器:`http://localhost:3000`
|
|
47
|
+
|
|
48
|
+
> 💡 第一次執行 `scan` 會產生 `galaxy.json`,`view` 會啟動伺服器讀取該檔案。
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 📦 How It Works
|
|
53
|
+
|
|
54
|
+
### 1️⃣ Scan Phase
|
|
55
|
+
|
|
56
|
+
- 解析 React 專案(支援 `.jsx`、`.tsx`、`.js`)
|
|
57
|
+
- 識別函式元件與類別元件
|
|
58
|
+
- 建立元件依賴圖(基於 import 與 JSX 標籤)
|
|
59
|
+
- 輸出 `galaxy.json`
|
|
60
|
+
|
|
61
|
+
### 2️⃣ View Phase
|
|
62
|
+
|
|
63
|
+
- 啟動本地 HTTP 伺服器
|
|
64
|
+
- 使用 `vis-network` 渲染力導向圖
|
|
65
|
+
- 節點大小、顏色對應行數與依賴數量
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 📁 Output Example
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
[
|
|
73
|
+
{
|
|
74
|
+
"name": "App",
|
|
75
|
+
"path": "src/App.jsx",
|
|
76
|
+
"lines": 24,
|
|
77
|
+
"dependencies": ["Header", "MainContent", "Footer"]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🛠️ Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git clone https://github.com/<你的用戶名>/react-galaxy.git
|
|
88
|
+
cd react-galaxy
|
|
89
|
+
npm install
|
|
90
|
+
npm link # 將指令 `galaxy` 連結到全域
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 🚀 Deploy to GitHub Pages
|
|
96
|
+
|
|
97
|
+
靜態展示檔案已放在 `docs/` 目錄,只需在倉庫設定中開啟 Pages:
|
|
98
|
+
|
|
99
|
+
1. 進入倉庫 → **Settings → Pages**
|
|
100
|
+
2. Branch: `main`,Folder: `/docs`
|
|
101
|
+
3. 點擊 **Save**
|
|
102
|
+
|
|
103
|
+
稍候一分鐘,即可透過 `https://[yourName].github.io/react-galaxy/` 訪問線上展示。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 🤝 Contributing
|
|
108
|
+
|
|
109
|
+
歡迎任何形式的貢獻 🙌
|
|
110
|
+
|
|
111
|
+
1. Fork 本倉庫
|
|
112
|
+
2. 建立新分支:`git checkout -b feature/amazing-feature`
|
|
113
|
+
3. 提交修改:`git commit -m "feat: add amazing feature"`
|
|
114
|
+
4. 推送分支:`git push origin feature/amazing-feature`
|
|
115
|
+
5. 開啟 Pull Request
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 🧭 Roadmap
|
|
120
|
+
|
|
121
|
+
- [ ] AST 精準解析(支援 TypeScript、HOC)
|
|
122
|
+
- [ ] 插件系統(自訂指標、輸出格式)
|
|
123
|
+
- [ ] AI 輔助分析(LLM 自動識別複雜元件)
|
|
124
|
+
- [ ] 遠端專案分析(上傳程式碼或 GitHub 連結)
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 📄 License
|
|
129
|
+
|
|
130
|
+
MIT License © 2026 [scyprodigy]
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analyze-components.mjs
|
|
3
|
+
*
|
|
4
|
+
* 使用 Babel 解析 React 專案中的 .jsx / .tsx 檔案,
|
|
5
|
+
* 輸出每個元件的名稱、路徑、行數與依賴列表。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* node analyze-components.mjs [srcDir] [outputFile]
|
|
9
|
+
*
|
|
10
|
+
* 範例:
|
|
11
|
+
* node analyze-components.mjs ./src galaxy.json
|
|
12
|
+
*
|
|
13
|
+
* 安裝依賴:
|
|
14
|
+
* npm install @babel/parser @babel/traverse glob
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import { createRequire } from "module";
|
|
20
|
+
import { glob } from "glob";
|
|
21
|
+
|
|
22
|
+
// @babel/traverse 只有 CJS export,用 createRequire 載入
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const parser = require("@babel/parser");
|
|
25
|
+
const traverseModule = require("@babel/traverse");
|
|
26
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
27
|
+
|
|
28
|
+
// ─── 常數 ────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const BABEL_PLUGINS = [
|
|
31
|
+
"jsx",
|
|
32
|
+
"typescript",
|
|
33
|
+
"classProperties",
|
|
34
|
+
"decorators-legacy",
|
|
35
|
+
"dynamicImport",
|
|
36
|
+
"optionalChaining",
|
|
37
|
+
"nullishCoalescingOperator",
|
|
38
|
+
"exportDefaultFrom",
|
|
39
|
+
"importAssertions",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// 只追蹤大寫開頭的標籤(React 元件慣例)
|
|
43
|
+
const isComponentName = (name) => /^[A-Z]/.test(name);
|
|
44
|
+
|
|
45
|
+
// ─── 解析單一檔案 ─────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} filePath 絕對路徑
|
|
49
|
+
* @param {string} rootDir 專案根目錄(用於計算相對路徑)
|
|
50
|
+
* @returns {Array<{name, path, lines, dependencies}>}
|
|
51
|
+
*/
|
|
52
|
+
function analyzeFile(filePath, rootDir) {
|
|
53
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
54
|
+
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
|
|
55
|
+
const totalLines = source.split("\n").length;
|
|
56
|
+
|
|
57
|
+
let ast;
|
|
58
|
+
try {
|
|
59
|
+
ast = parser.parse(source, {
|
|
60
|
+
sourceType: "module",
|
|
61
|
+
plugins: BABEL_PLUGINS,
|
|
62
|
+
errorRecovery: true,
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.warn(`⚠️ 無法解析 ${relativePath}:${err.message}`);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── 1. 收集所有 import 資訊 ──────────────────────────────────────────────
|
|
70
|
+
// importedNames: Set<string> — 此檔案 import 的所有識別符
|
|
71
|
+
// importMap: Map<localName, source>
|
|
72
|
+
const importedNames = new Set();
|
|
73
|
+
const importMap = new Map(); // localName → import source
|
|
74
|
+
|
|
75
|
+
traverse(ast, {
|
|
76
|
+
ImportDeclaration({ node }) {
|
|
77
|
+
for (const specifier of node.specifiers) {
|
|
78
|
+
const local = specifier.local.name;
|
|
79
|
+
importedNames.add(local);
|
|
80
|
+
importMap.set(local, node.source.value);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── 2. 識別元件定義 ───────────────────────────────────────────────────────
|
|
86
|
+
// 收集每個元件:{ name, startLine, endLine, jsxTags: Set<string> }
|
|
87
|
+
const componentMap = new Map(); // name → component info
|
|
88
|
+
|
|
89
|
+
const registerComponent = (name, startLine, endLine) => {
|
|
90
|
+
if (!name || !isComponentName(name)) return;
|
|
91
|
+
if (!componentMap.has(name)) {
|
|
92
|
+
componentMap.set(name, {
|
|
93
|
+
name,
|
|
94
|
+
startLine,
|
|
95
|
+
endLine,
|
|
96
|
+
jsxTags: new Set(),
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
// 同名多次定義時,取最後一個(override / HOC pattern)
|
|
100
|
+
const existing = componentMap.get(name);
|
|
101
|
+
existing.startLine = startLine;
|
|
102
|
+
existing.endLine = endLine;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
traverse(ast, {
|
|
107
|
+
// function Component() {} / export default function Component() {}
|
|
108
|
+
FunctionDeclaration({ node }) {
|
|
109
|
+
if (node.id) {
|
|
110
|
+
registerComponent(
|
|
111
|
+
node.id.name,
|
|
112
|
+
node.loc?.start.line,
|
|
113
|
+
node.loc?.end.line
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// const Component = () => {} / const Component = function() {}
|
|
119
|
+
VariableDeclarator({ node }) {
|
|
120
|
+
if (
|
|
121
|
+
node.id?.type === "Identifier" &&
|
|
122
|
+
isComponentName(node.id.name) &&
|
|
123
|
+
(node.init?.type === "ArrowFunctionExpression" ||
|
|
124
|
+
node.init?.type === "FunctionExpression")
|
|
125
|
+
) {
|
|
126
|
+
registerComponent(
|
|
127
|
+
node.id.name,
|
|
128
|
+
node.loc?.start.line,
|
|
129
|
+
node.loc?.end.line
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// class Component extends React.Component / Component
|
|
135
|
+
ClassDeclaration({ node }) {
|
|
136
|
+
if (node.id) {
|
|
137
|
+
registerComponent(
|
|
138
|
+
node.id.name,
|
|
139
|
+
node.loc?.start.line,
|
|
140
|
+
node.loc?.end.line
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// export default class / export default function
|
|
146
|
+
ExportDefaultDeclaration({ node }) {
|
|
147
|
+
const decl = node.declaration;
|
|
148
|
+
if (
|
|
149
|
+
(decl.type === "FunctionDeclaration" ||
|
|
150
|
+
decl.type === "ClassDeclaration") &&
|
|
151
|
+
decl.id
|
|
152
|
+
) {
|
|
153
|
+
registerComponent(
|
|
154
|
+
decl.id.name,
|
|
155
|
+
decl.loc?.start.line,
|
|
156
|
+
decl.loc?.end.line
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ── 3. 收集 JSX 中使用的元件標籤 ─────────────────────────────────────────
|
|
163
|
+
// 策略:找出每個 JSXOpeningElement,判斷它屬於哪個元件定義的範圍
|
|
164
|
+
traverse(ast, {
|
|
165
|
+
JSXOpeningElement({ node }) {
|
|
166
|
+
let tagName = null;
|
|
167
|
+
if (node.name.type === "JSXIdentifier") {
|
|
168
|
+
tagName = node.name.name;
|
|
169
|
+
} else if (node.name.type === "JSXMemberExpression") {
|
|
170
|
+
// e.g. <UI.Button> → 取最外層物件名
|
|
171
|
+
let obj = node.name;
|
|
172
|
+
while (obj.type === "JSXMemberExpression") obj = obj.object;
|
|
173
|
+
tagName = obj.name;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!tagName || !isComponentName(tagName)) return;
|
|
177
|
+
|
|
178
|
+
const line = node.loc?.start.line ?? 0;
|
|
179
|
+
|
|
180
|
+
// 找最近包圍此 JSX 的元件
|
|
181
|
+
let bestComp = null;
|
|
182
|
+
for (const comp of componentMap.values()) {
|
|
183
|
+
if (
|
|
184
|
+
comp.startLine <= line &&
|
|
185
|
+
line <= comp.endLine &&
|
|
186
|
+
(!bestComp ||
|
|
187
|
+
comp.endLine - comp.startLine <
|
|
188
|
+
bestComp.endLine - bestComp.startLine)
|
|
189
|
+
) {
|
|
190
|
+
bestComp = comp;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (bestComp && tagName !== bestComp.name) {
|
|
195
|
+
bestComp.jsxTags.add(tagName);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ── 4. 組合輸出 ───────────────────────────────────────────────────────────
|
|
201
|
+
const results = [];
|
|
202
|
+
|
|
203
|
+
for (const comp of componentMap.values()) {
|
|
204
|
+
// 依賴 = JSX 中使用且在 importedNames 裡的元件名稱
|
|
205
|
+
// (過濾掉同檔案內的輔助元件)
|
|
206
|
+
const dependencies = [...comp.jsxTags].filter((tag) =>
|
|
207
|
+
importedNames.has(tag)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
results.push({
|
|
211
|
+
name: comp.name,
|
|
212
|
+
path: relativePath,
|
|
213
|
+
lines: (comp.endLine ?? totalLines) - (comp.startLine ?? 1) + 1,
|
|
214
|
+
dependencies: [...new Set(dependencies)].sort(),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── 掃描目錄 ─────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
async function analyzeProject(srcDir, outputFile) {
|
|
224
|
+
const absoluteSrc = path.resolve(srcDir);
|
|
225
|
+
|
|
226
|
+
console.log(`🔍 掃描目錄:${absoluteSrc}`);
|
|
227
|
+
|
|
228
|
+
const files = await glob("**/*.{js,jsx,ts,tsx}", {
|
|
229
|
+
cwd: absoluteSrc,
|
|
230
|
+
absolute: true,
|
|
231
|
+
ignore: ["**/node_modules/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*"],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
console.log(`📄 找到 ${files.length} 個檔案`);
|
|
235
|
+
|
|
236
|
+
const allComponents = [];
|
|
237
|
+
|
|
238
|
+
for (const file of files) {
|
|
239
|
+
const components = analyzeFile(file, path.resolve("."));
|
|
240
|
+
allComponents.push(...components);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 按元件名稱排序,方便閱讀
|
|
244
|
+
allComponents.sort((a, b) => a.name.localeCompare(b.name));
|
|
245
|
+
|
|
246
|
+
const json = JSON.stringify(allComponents, null, 2);
|
|
247
|
+
|
|
248
|
+
if (outputFile) {
|
|
249
|
+
fs.writeFileSync(outputFile, json, "utf-8");
|
|
250
|
+
console.log(`✅ 輸出至 ${outputFile}(共 ${allComponents.length} 個元件)`);
|
|
251
|
+
} else {
|
|
252
|
+
console.log(json);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return allComponents;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─── CLI 入口 ─────────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
const [, , srcArg = "./src", outArg = "galaxy.json"] = process.argv;
|
|
261
|
+
await analyzeProject(srcArg, outArg);
|
package/bin/galaxy.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('galaxy')
|
|
18
|
+
.description('React component dependency visualizer')
|
|
19
|
+
.version('1.0.0');
|
|
20
|
+
|
|
21
|
+
// ====== scan 指令 ======
|
|
22
|
+
function scanProject(dir, outputFile = 'galaxy.json') {
|
|
23
|
+
const scriptPath = path.join(projectRoot, 'analyze-components.mjs');
|
|
24
|
+
const args = [scriptPath, dir, outputFile];
|
|
25
|
+
|
|
26
|
+
console.log(`🔍 掃描目錄:${dir} → 輸出 ${outputFile}`);
|
|
27
|
+
|
|
28
|
+
const child = spawn('node', args, { stdio: 'inherit' });
|
|
29
|
+
|
|
30
|
+
child.on('error', (err) => {
|
|
31
|
+
console.error(`❌ 無法執行掃描: ${err.message}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.on('exit', (code) => {
|
|
35
|
+
if (code === 0) {
|
|
36
|
+
console.log(`✅ 掃描完成,結果已寫入 ${outputFile}`);
|
|
37
|
+
} else {
|
|
38
|
+
console.error(`❌ 掃描失敗,退出碼 ${code}`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command('scan <directory>')
|
|
45
|
+
.description('Scan a React project directory and generate galaxy.json')
|
|
46
|
+
.option('-o, --output <file>', 'output file name', 'galaxy.json')
|
|
47
|
+
.action((directory, options) => {
|
|
48
|
+
scanProject(directory, options.output);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ====== view 指令(可視化伺服器) ======
|
|
52
|
+
program
|
|
53
|
+
.command('view')
|
|
54
|
+
.description('Start local viewer server')
|
|
55
|
+
.action(() => {
|
|
56
|
+
const port = 3000;
|
|
57
|
+
const galaxyJsonPath = path.join(process.cwd(), 'galaxy.json');
|
|
58
|
+
|
|
59
|
+
const server = http.createServer((req, res) => {
|
|
60
|
+
if (req.url === '/galaxy.json') {
|
|
61
|
+
// 提供 JSON 數據
|
|
62
|
+
fs.readFile(galaxyJsonPath, 'utf8', (err, data) => {
|
|
63
|
+
if (err) {
|
|
64
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
65
|
+
res.end(JSON.stringify({ error: 'galaxy.json not found. Run `galaxy scan` first.' }));
|
|
66
|
+
} else {
|
|
67
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
68
|
+
res.end(data);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
// 提供 HTML 頁面
|
|
73
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
74
|
+
res.end(`
|
|
75
|
+
<!DOCTYPE html>
|
|
76
|
+
<html>
|
|
77
|
+
<head>
|
|
78
|
+
<title>React Galaxy - Component Dependency</title>
|
|
79
|
+
<meta charset="utf-8">
|
|
80
|
+
<style>
|
|
81
|
+
body { margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
|
|
82
|
+
#network { width: 100vw; height: 100vh; background: #0a0a1a; }
|
|
83
|
+
.info {
|
|
84
|
+
position: absolute;
|
|
85
|
+
bottom: 10px;
|
|
86
|
+
left: 10px;
|
|
87
|
+
background: rgba(0,0,0,0.7);
|
|
88
|
+
color: #ccc;
|
|
89
|
+
padding: 5px 10px;
|
|
90
|
+
border-radius: 5px;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
z-index: 10;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
96
|
+
<script type="text/javascript" src="https://unpkg.com/vis-network@9.1.2/dist/vis-network.min.js"></script>
|
|
97
|
+
</head>
|
|
98
|
+
<body>
|
|
99
|
+
<div id="network"></div>
|
|
100
|
+
<div class="info">
|
|
101
|
+
🌌 React Galaxy | 節點大小代表元件行數 | 邊表示依賴關係
|
|
102
|
+
</div>
|
|
103
|
+
<script>
|
|
104
|
+
fetch('/galaxy.json')
|
|
105
|
+
.then(res => res.json())
|
|
106
|
+
.then(data => {
|
|
107
|
+
// 構建節點和邊
|
|
108
|
+
const nodes = [];
|
|
109
|
+
const edges = [];
|
|
110
|
+
const nodeMap = new Map();
|
|
111
|
+
|
|
112
|
+
data.forEach(comp => {
|
|
113
|
+
const id = comp.name;
|
|
114
|
+
nodeMap.set(id, {
|
|
115
|
+
id: id,
|
|
116
|
+
label: comp.name,
|
|
117
|
+
title: \`路徑: \${comp.path}\\n行數: \${comp.lines}\\n依賴: \${comp.dependencies.join(', ') || '無'}\`,
|
|
118
|
+
value: comp.lines, // 用行數決定節點大小
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 添加節點
|
|
123
|
+
for (let node of nodeMap.values()) {
|
|
124
|
+
nodes.push(node);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 添加邊
|
|
128
|
+
data.forEach(comp => {
|
|
129
|
+
const from = comp.name;
|
|
130
|
+
comp.dependencies.forEach(to => {
|
|
131
|
+
if (nodeMap.has(to)) {
|
|
132
|
+
edges.push({ from, to, arrows: 'to', smooth: { type: 'curvedCW' } });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const container = document.getElementById('network');
|
|
138
|
+
const options = {
|
|
139
|
+
nodes: {
|
|
140
|
+
shape: 'dot',
|
|
141
|
+
scaling: {
|
|
142
|
+
min: 10,
|
|
143
|
+
max: 60,
|
|
144
|
+
label: { enabled: true, min: 12, max: 30 }
|
|
145
|
+
},
|
|
146
|
+
font: { color: '#ffffff', size: 14, face: 'monospace' },
|
|
147
|
+
borderWidth: 2,
|
|
148
|
+
shadow: true
|
|
149
|
+
},
|
|
150
|
+
edges: {
|
|
151
|
+
color: { color: '#88aaff', highlight: '#ffaa88' },
|
|
152
|
+
width: 2,
|
|
153
|
+
smooth: { type: 'continuous', roundness: 0.5 },
|
|
154
|
+
arrows: { to: { enabled: true, scaleFactor: 0.8 } }
|
|
155
|
+
},
|
|
156
|
+
physics: {
|
|
157
|
+
stabilization: { iterations: 200 },
|
|
158
|
+
barnesHut: { gravitationalConstant: -8000, centralGravity: 0.3, springLength: 150 }
|
|
159
|
+
},
|
|
160
|
+
interaction: {
|
|
161
|
+
hover: true,
|
|
162
|
+
tooltipDelay: 200,
|
|
163
|
+
zoomView: true,
|
|
164
|
+
dragView: true
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const network = new vis.Network(container, { nodes, edges }, options);
|
|
168
|
+
})
|
|
169
|
+
.catch(err => {
|
|
170
|
+
console.error(err);
|
|
171
|
+
document.body.innerHTML = '<h2 style="color:red;padding:20px">❌ 無法載入 galaxy.json,請先執行 <code>galaxy scan ./src</code></h2>';
|
|
172
|
+
});
|
|
173
|
+
</script>
|
|
174
|
+
</body>
|
|
175
|
+
</html>
|
|
176
|
+
`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
server.listen(port, () => {
|
|
181
|
+
console.log(`🚀 Galaxy Viewer 啟動:http://localhost:${port}`);
|
|
182
|
+
console.log(`📄 讀取數據:${galaxyJsonPath}`);
|
|
183
|
+
if (!fs.existsSync(galaxyJsonPath)) {
|
|
184
|
+
console.warn(`⚠️ 找不到 galaxy.json,請先執行掃描。`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-dep-galaxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Visualize React component dependencies as an interactive galaxy",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"galaxy": "./bin/galaxy.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"analyze-components.mjs",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@babel/parser": "^7.23.0",
|
|
17
|
+
"@babel/traverse": "^7.23.0",
|
|
18
|
+
"commander": "^12.0.0",
|
|
19
|
+
"glob": "^10.3.10"
|
|
20
|
+
}
|
|
21
|
+
}
|