@zhaofx/deplens 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/bin/assets/depLens.ico +0 -0
- package/bin/assets/depLens.png +0 -0
- package/bin/config.js +5 -0
- package/bin/diffView.js +304 -0
- package/bin/env-checker.js +188 -0
- package/bin/env-recorder.js +58 -0
- package/bin/index.js +84 -0
- package/bin/template.html +684 -0
- package/package.json +39 -0
- package/readme.md +92 -0
|
Binary file
|
|
Binary file
|
package/bin/config.js
ADDED
package/bin/diffView.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import yaml from 'yaml';
|
|
5
|
+
import polka from 'polka';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import { glob } from 'glob';
|
|
10
|
+
import * as parser from '@babel/parser';
|
|
11
|
+
import traverseModule from '@babel/traverse';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
const traverse = traverseModule.default || traverseModule;
|
|
15
|
+
const st = promisify(fs.stat);
|
|
16
|
+
const readDir = promisify(fs.readdir);
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
// 1. 获取物理体积 (不变)
|
|
20
|
+
async function getDirSize(dirPath) {
|
|
21
|
+
let size = 0;
|
|
22
|
+
try {
|
|
23
|
+
const files = await readDir(dirPath);
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const filePath = path.join(dirPath, file);
|
|
26
|
+
const stats = await st(filePath);
|
|
27
|
+
if (stats.isDirectory()) {
|
|
28
|
+
size += await getDirSize(filePath);
|
|
29
|
+
} else {
|
|
30
|
+
size += stats.size;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (err) { }
|
|
34
|
+
return size;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 获取当前环境的版本信息
|
|
39
|
+
*/
|
|
40
|
+
function getEnvInfo() {
|
|
41
|
+
try {
|
|
42
|
+
const nodeVersion = process.version; // 直接从 process 获取
|
|
43
|
+
const npmVersion = execSync('npm -v').toString().trim();
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
node: nodeVersion,
|
|
47
|
+
npm: npmVersion,
|
|
48
|
+
platform: process.platform,
|
|
49
|
+
arch: process.arch
|
|
50
|
+
};
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error("无法获取版本信息:", e);
|
|
53
|
+
return { node: process.version, npm: "unknown" };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatSize(bytes) {
|
|
58
|
+
if (bytes === 0 || !bytes) return '0 B';
|
|
59
|
+
const k = 1024;
|
|
60
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
61
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
62
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. AST 扫描 (不变)
|
|
66
|
+
async function scanSourceImports(rootDir) {
|
|
67
|
+
const importsMap = {};
|
|
68
|
+
console.log('正在进行 AST 源码扫描...');
|
|
69
|
+
const files = await glob('src/**/*.{js,ts,jsx,tsx}', { cwd: rootDir, absolute: true });
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
try {
|
|
72
|
+
const code = await fs.readFile(file, 'utf8');
|
|
73
|
+
const ast = parser.parse(code, { sourceType: 'module', plugins: ['typescript', 'jsx', 'decorators-legacy'] });
|
|
74
|
+
const relPath = path.relative(rootDir, file);
|
|
75
|
+
traverse(ast, {
|
|
76
|
+
ImportDeclaration({ node }) {
|
|
77
|
+
if (!node.source.value.startsWith('.')) {
|
|
78
|
+
const parts = node.source.value.split('/');
|
|
79
|
+
const pkgName = node.source.value.startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
80
|
+
if (!importsMap[pkgName]) importsMap[pkgName] = [];
|
|
81
|
+
if (!importsMap[pkgName].includes(relPath)) importsMap[pkgName].push(relPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
} catch (err) { }
|
|
86
|
+
}
|
|
87
|
+
return importsMap;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parsePnpmKey(key) {
|
|
91
|
+
const noSlash = key.startsWith('/') ? key.slice(1) : key;
|
|
92
|
+
const parts = noSlash.split('@');
|
|
93
|
+
let name, versionStr;
|
|
94
|
+
if (noSlash.startsWith('@')) {
|
|
95
|
+
name = '@' + parts[1];
|
|
96
|
+
versionStr = parts.slice(2).join('@');
|
|
97
|
+
} else {
|
|
98
|
+
name = parts[0];
|
|
99
|
+
versionStr = parts.slice(1).join('@');
|
|
100
|
+
}
|
|
101
|
+
return { name, version: versionStr.split('(')[0] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function start() {
|
|
105
|
+
const rootDir = process.cwd();
|
|
106
|
+
const lockPath = path.join(rootDir, 'pnpm-lock.yaml');
|
|
107
|
+
const pkgPath = path.join(rootDir, 'package.json');
|
|
108
|
+
const pnpmStorePath = path.join(rootDir, 'node_modules/.pnpm');
|
|
109
|
+
|
|
110
|
+
if (!fs.existsSync(lockPath) || !fs.existsSync(pkgPath)) {
|
|
111
|
+
console.error('❌ 错误: 未发现 pnpm-lock.yaml 或 package.json');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pkgData = fs.readJsonSync(pkgPath);
|
|
116
|
+
const rootDeps = { ...(pkgData.dependencies || {}), ...(pkgData.devDependencies || {}) };
|
|
117
|
+
const declaredDeps = new Set(Object.keys(rootDeps));
|
|
118
|
+
|
|
119
|
+
const realImports = await scanSourceImports(rootDir);
|
|
120
|
+
const lockData = yaml.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
121
|
+
const snapshots = lockData.snapshots || lockData.packages || {};
|
|
122
|
+
|
|
123
|
+
console.log('正在解析 Lockfile 并计算磁盘体积...');
|
|
124
|
+
|
|
125
|
+
// 3. 将所有包缓存,方便构建树时快速查找
|
|
126
|
+
const pkgCache = {};
|
|
127
|
+
for (const key of Object.keys(snapshots)) {
|
|
128
|
+
const { name, version } = parsePnpmKey(key);
|
|
129
|
+
const pnpmName = name.replace(/\//g, '+');
|
|
130
|
+
const possiblePaths = [
|
|
131
|
+
path.join(pnpmStorePath, `${pnpmName}@${version}`, 'node_modules', name),
|
|
132
|
+
path.join(pnpmStorePath, `${pnpmName}@${version.split('_')[0]}`, 'node_modules', name)
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
let size = 0;
|
|
136
|
+
for (const p of possiblePaths) {
|
|
137
|
+
if (fs.existsSync(p)) { size = await getDirSize(p); break; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pkgCache[`${name}@${version}`] = {
|
|
141
|
+
name, version,
|
|
142
|
+
snapshot: snapshots[key],
|
|
143
|
+
size: size > 0 ? size : 1024,
|
|
144
|
+
isConflict: false // 简写
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 4. 【核心新增】递归构建嵌套依赖树
|
|
149
|
+
function buildDependencyTree(depsObj, visited = new Set()) {
|
|
150
|
+
const children = [];
|
|
151
|
+
for (const [depName, depVerReq] of Object.entries(depsObj)) {
|
|
152
|
+
// 模糊匹配:尝试找精确版本,找不到就取同名包的第一个
|
|
153
|
+
let match = pkgCache[`${depName}@${depVerReq}`] || Object.values(pkgCache).find(p => p.name === depName);
|
|
154
|
+
if (!match) continue;
|
|
155
|
+
|
|
156
|
+
const nodeKey = `${match.name}@${match.version}`;
|
|
157
|
+
// 防止循环依赖死循环
|
|
158
|
+
if (visited.has(nodeKey)) continue;
|
|
159
|
+
|
|
160
|
+
const newVisited = new Set(visited);
|
|
161
|
+
newVisited.add(nodeKey);
|
|
162
|
+
|
|
163
|
+
const subDeps = match.snapshot.dependencies || {};
|
|
164
|
+
const childNodes = buildDependencyTree(subDeps, newVisited);
|
|
165
|
+
|
|
166
|
+
const node = {
|
|
167
|
+
name: match.name,
|
|
168
|
+
version: match.version,
|
|
169
|
+
originalSize: match.size,
|
|
170
|
+
formattedSize: formatSize(match.size)
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (childNodes.length > 0) {
|
|
174
|
+
// 如果有子依赖,则当前包本身的代码作为 "自身(Self)" 节点展示
|
|
175
|
+
node.children = [
|
|
176
|
+
{
|
|
177
|
+
name: `${match.name} (自身)`,
|
|
178
|
+
version: match.version,
|
|
179
|
+
size: match.size,
|
|
180
|
+
formattedSize: formatSize(match.size),
|
|
181
|
+
isSelf: true
|
|
182
|
+
},
|
|
183
|
+
...childNodes
|
|
184
|
+
];
|
|
185
|
+
} else {
|
|
186
|
+
node.size = match.size;
|
|
187
|
+
}
|
|
188
|
+
children.push(node);
|
|
189
|
+
}
|
|
190
|
+
return children;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const rootChildren = buildDependencyTree(rootDeps);
|
|
194
|
+
|
|
195
|
+
// 5. 挂载幽灵依赖 (源码中引入了,但未在 package.json 声明的)
|
|
196
|
+
for (const ghostName of Object.keys(realImports)) {
|
|
197
|
+
if (!declaredDeps.has(ghostName)) {
|
|
198
|
+
const match = Object.values(pkgCache).find(p => p.name === ghostName);
|
|
199
|
+
if (match) {
|
|
200
|
+
rootChildren.push({
|
|
201
|
+
name: match.name,
|
|
202
|
+
version: match.version,
|
|
203
|
+
size: match.size,
|
|
204
|
+
formattedSize: formatSize(match.size),
|
|
205
|
+
isGhost: true,
|
|
206
|
+
ghostSources: realImports[ghostName]
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const graphData = { name: pkgData.name || 'Project Root', children: rootChildren, environment: getEnvInfo() };
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 尝试启动服务
|
|
218
|
+
* @param {number} port 初始端口
|
|
219
|
+
*/
|
|
220
|
+
function startDepLens(currentPort, maxTries = 10, originalPort = currentPort) {
|
|
221
|
+
// 记录一个标记,防止在错误处理后再触发回调逻辑
|
|
222
|
+
const app = polka();
|
|
223
|
+
app.get('/data.js', (req, res) => {
|
|
224
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
225
|
+
return res.end(`window.CHART_DATA = ${JSON.stringify(graphData)};`);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ico
|
|
229
|
+
app.get('/favicon.png', async (req, res) => {
|
|
230
|
+
res.setHeader('Content-Type', 'image/png');
|
|
231
|
+
const imgPath = path.join(__dirname, './assets/depLens.png'); // 图片实际路径
|
|
232
|
+
const img = await fs.readFile(imgPath); // 默认返回 Buffer
|
|
233
|
+
|
|
234
|
+
res.setHeader('Content-Type', 'image/png');
|
|
235
|
+
return res.end(img); // 直接发送 Buffer 数据
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
app.get('/', (req, res) => {
|
|
239
|
+
return res.end(fs.readFileSync(path.join(__dirname, 'template.html'), 'utf8'));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// 修复:增加一个简单的树平铺函数
|
|
243
|
+
function flattenTree(children, result = []) {
|
|
244
|
+
for (const child of children) {
|
|
245
|
+
// 只记录真实的包,跳过我们为了显示体积而伪造的 "(自身)" 节点
|
|
246
|
+
if (!child.isSelf) {
|
|
247
|
+
result.push({ name: child.name, version: child.version });
|
|
248
|
+
}
|
|
249
|
+
if (child.children) {
|
|
250
|
+
flattenTree(child.children, result);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
app.get('/export', (req, res) => {
|
|
257
|
+
res.setHeader('Content-Type', 'application/json');
|
|
258
|
+
|
|
259
|
+
// 使用 flattenTree 处理构建好的 rootChildren
|
|
260
|
+
const snapshot = flattenTree(rootChildren);
|
|
261
|
+
|
|
262
|
+
// 去重:因为同一个包可能在树的不同层级出现多次
|
|
263
|
+
const uniqueSnapshot = Array.from(new Set(snapshot.map(s => `${s.name}@${s.version}`)))
|
|
264
|
+
.map(str => {
|
|
265
|
+
const [name, version] = str.split('@');
|
|
266
|
+
return { name, version };
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return res.end(JSON.stringify(uniqueSnapshot));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const instance = app.listen(currentPort, (err) => {
|
|
273
|
+
if (err) return;
|
|
274
|
+
|
|
275
|
+
const address = instance.server.address();
|
|
276
|
+
const actualPort = address ? address.port : currentPort;
|
|
277
|
+
|
|
278
|
+
console.log(`🚀 DepLens 嵌套层级诊断已启动: http://localhost:${actualPort}`);
|
|
279
|
+
|
|
280
|
+
if (actualPort !== originalPort) {
|
|
281
|
+
console.log(`💡 注意:初始端口 ${originalPort} 被占用,已自动切换至 ${actualPort}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 必须使用实际监听的端口打开
|
|
285
|
+
open(`http://localhost:${actualPort}`);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
instance.server.on('error', (e) => {
|
|
289
|
+
if (e.code === 'EADDRINUSE') {
|
|
290
|
+
console.log(`端口 ${currentPort} 被占用,尝试使用下一个端口...`);
|
|
291
|
+
setTimeout(() => {
|
|
292
|
+
startDepLens(currentPort + 1, maxTries, originalPort);
|
|
293
|
+
}, 200);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
console.error(`❌ 服务器发生不可用错误: ${e.message}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
const port = 3456; // 默认端口
|
|
301
|
+
startDepLens(port);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export default start;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { packageManagers } from './config.js';
|
|
5
|
+
import recordEnv from './env-recorder.js';
|
|
6
|
+
import inquirer from 'inquirer'; // 建议安装:npm install inquirer
|
|
7
|
+
/**
|
|
8
|
+
* 获取实时环境信息(用于对比)
|
|
9
|
+
*/
|
|
10
|
+
function getCurrentEnv() {
|
|
11
|
+
const getV = (cmd) => {
|
|
12
|
+
try { return execSync(cmd).toString().trim(); }
|
|
13
|
+
catch { return 'N/A'; }
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
node: process.version,
|
|
18
|
+
npm: getV('npm -v'),
|
|
19
|
+
pnpm: getV('pnpm -v'),
|
|
20
|
+
yarn: getV('yarn -v'),
|
|
21
|
+
registry: getV('pnpm config get registry') || getV('npm config get registry'),
|
|
22
|
+
os: `${process.platform} (${process.arch})`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* depLens -check 核心功能
|
|
28
|
+
*/
|
|
29
|
+
export async function checkEnv() {
|
|
30
|
+
const historyPath = path.join(process.cwd(), '.deplens.history.json');
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(historyPath)) {
|
|
33
|
+
console.warn('⚠️ 未发现 .deplens.history.json,请先运行 depLens -track 初始化项目标准。');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const history = JSON.parse(fs.readFileSync(historyPath, 'utf8'));
|
|
38
|
+
const standard = history[history.length - 1]; // 取最后一次记录作为标准
|
|
39
|
+
const current = getCurrentEnv(); // 获取当前环境信息
|
|
40
|
+
|
|
41
|
+
const errors = [];
|
|
42
|
+
const warnings = [];
|
|
43
|
+
|
|
44
|
+
// 1. 强校验:Node 主版本 (Major Version)
|
|
45
|
+
if (standard.node.split('.')[0] !== current.node.split('.')[0]) {
|
|
46
|
+
errors.push(`Node 版本不匹配: 预期 ${standard.node}, 当前 ${current.node}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. 强校验:包管理器 (Active Manager)
|
|
50
|
+
if (packageManagers.includes(standard.activeManager) && current[standard.activeManager] === 'N/A') {
|
|
51
|
+
errors.push(`缺失关键工具: 项目指定使用 ${standard.activeManager},但本地未安装。`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. 强校验:镜像源 (防止 Lockfile 污染)
|
|
55
|
+
// 统一去掉末尾斜杠对比
|
|
56
|
+
const normalizeUrl = (url) => url.replace(/\/$/, '');
|
|
57
|
+
if (normalizeUrl(standard.registry.pnpm) !== normalizeUrl(current.registry)) {
|
|
58
|
+
errors.push(`Registry 不一致: \n 标准: ${standard.registry.pnpm}\n 当前: ${current.registry}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 4. 弱校验:系统平台
|
|
62
|
+
if (standard.os !== current.os) {
|
|
63
|
+
warnings.push(`操作系统差异: 项目标准为 ${standard.os}, 当前为 ${current.os}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- 输出结果 ---
|
|
67
|
+
if (errors.length > 0) {
|
|
68
|
+
console.error('\n❌ 【DepLens 环境检查失败】');
|
|
69
|
+
errors.forEach(err => console.error(` - ${err}`));
|
|
70
|
+
console.error('\n原因:环境不一致可能导致依赖安装失败、编译错误或 Lockfile 冲突。');
|
|
71
|
+
console.error('建议:请切换 Node 版本或修改 npm/pnpm registry。');
|
|
72
|
+
|
|
73
|
+
// 如果是 CI 环境或强制检查模式,阻断后续流程
|
|
74
|
+
if (process.env.CI || process.argv.includes('--strict')) {
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.log('✅ 【DepLens】环境检查通过:本地环境与项目标准一致。');
|
|
79
|
+
if (warnings.length > 0) {
|
|
80
|
+
warnings.forEach(w => console.log(` ⚠️ 注意: ${w}`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* depLens -install 核心实现
|
|
89
|
+
* 自动识别历史记录中的包管理器并执行安装
|
|
90
|
+
*/
|
|
91
|
+
export async function autoInstall() {
|
|
92
|
+
const historyPath = path.join(process.cwd(), '.deplens.history.json');
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(historyPath)) {
|
|
95
|
+
console.error('❌ 错误: 未发现 .deplens.history.json。请先运行 depLens -track 初始化环境快照。');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const history = JSON.parse(fs.readFileSync(historyPath, 'utf8'));
|
|
100
|
+
// 获取最新的一条环境快照
|
|
101
|
+
const standard = history[history.length - 1];
|
|
102
|
+
const manager = standard.activeManager || 'npm';
|
|
103
|
+
|
|
104
|
+
console.log(`📦 检测到项目标准包管理器为: ${manager}`);
|
|
105
|
+
console.log(`🚀 正在准备执行: ${manager} install...`);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// 1. 检查本地是否安装了该管理器
|
|
109
|
+
try {
|
|
110
|
+
execSync(`${manager} -v`, { stdio: 'ignore' });
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.warn(`⚠️ 本地未检测到 ${manager},尝试全局安装...`);
|
|
113
|
+
execSync(`npm install -g ${manager}`, { stdio: 'inherit' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. 检查 Registry 并在安装前提示
|
|
117
|
+
const currentRegistry = execSync(`${manager} config get registry`).toString().trim();
|
|
118
|
+
const standardRegistry = standard.registry[manager] || standard.registry.npm;
|
|
119
|
+
|
|
120
|
+
if (standardRegistry && standardRegistry !== 'N/A' && currentRegistry !== standardRegistry) {
|
|
121
|
+
console.log(`🌐 正在同步 Registry 镜像源至: ${standardRegistry}`);
|
|
122
|
+
execSync(`${manager} config set registry ${standardRegistry}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 3. 执行安装
|
|
126
|
+
console.log(`\n🛠️ 开始安装依赖...\n`);
|
|
127
|
+
execSync(`${manager} install`, { stdio: 'inherit' });
|
|
128
|
+
|
|
129
|
+
console.log(`\n✅ 依赖安装完成!`);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(`\n❌ 安装过程中出现错误: ${err.message}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 探测当前项目最匹配的包管理器
|
|
139
|
+
*/
|
|
140
|
+
function detectManager(rootDir) {
|
|
141
|
+
if (fs.existsSync(path.join(rootDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
142
|
+
if (fs.existsSync(path.join(rootDir, 'package-lock.json'))) return 'npm';
|
|
143
|
+
if (fs.existsSync(path.join(rootDir, 'yarn.lock'))) return 'yarn';
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* depLens -init 实现
|
|
149
|
+
*/
|
|
150
|
+
export async function initProject() {
|
|
151
|
+
const rootDir = process.cwd();
|
|
152
|
+
const historyPath = path.join(rootDir, '.deplens.history.json');
|
|
153
|
+
|
|
154
|
+
console.log('🔍 DepLens 正在分析项目结构...');
|
|
155
|
+
|
|
156
|
+
// 1. 自动探测
|
|
157
|
+
const detected = detectManager(rootDir);
|
|
158
|
+
|
|
159
|
+
// 2. 交互式选择
|
|
160
|
+
const answers = await inquirer.prompt([
|
|
161
|
+
{
|
|
162
|
+
type: 'list',
|
|
163
|
+
name: 'manager',
|
|
164
|
+
message: '请确认该项目指定的标准包管理器(node:v16.9+ 内置Corepack,无需额外安装 pnpm/yarn ,低于v16.9版本请手动开启:corepack enable):',
|
|
165
|
+
choices: [
|
|
166
|
+
{ name: 'pnpm (推荐)', value: 'pnpm' },
|
|
167
|
+
{ name: 'npm', value: 'npm' },
|
|
168
|
+
{ name: 'yarn', value: 'yarn' },
|
|
169
|
+
{ name: '不锁定 (不建议)', value: null }
|
|
170
|
+
],
|
|
171
|
+
default: detected // 默认选中当前项目匹配的
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'confirm',
|
|
175
|
+
name: 'saveHistory',
|
|
176
|
+
message: '是否立即生成当前环境快照 (.deplens.history.json)?',
|
|
177
|
+
default: true
|
|
178
|
+
}
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
// 3. 执行 track 逻辑并生成文件
|
|
182
|
+
if (answers.saveHistory) {
|
|
183
|
+
await recordEnv(); // 记录当前环境快照
|
|
184
|
+
console.log('\n✅ 初始化成功!');
|
|
185
|
+
console.log(`📝 已创建标准快照,指定包管理器为: ${answers.manager || '未指定'}`);
|
|
186
|
+
console.log('💡 建议将 .deplens.history.json 提交至 Git 仓库以同步团队环境。');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
function getSafeExec(cmd) {
|
|
6
|
+
try {
|
|
7
|
+
return execSync(cmd).toString().trim();
|
|
8
|
+
} catch (e) {
|
|
9
|
+
return 'N/A';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function recordEnv() {
|
|
14
|
+
// 1. 基础环境
|
|
15
|
+
const info = {
|
|
16
|
+
timestamp: new Date().toLocaleString(),
|
|
17
|
+
user: getSafeExec('git config user.name') || 'unknown',
|
|
18
|
+
os: `${process.platform} (${process.arch})`,
|
|
19
|
+
node: process.version,
|
|
20
|
+
// 2. 包管理器版本
|
|
21
|
+
npm: getSafeExec('npm -v'),
|
|
22
|
+
pnpm: getSafeExec('pnpm -v'),
|
|
23
|
+
yarn: getSafeExec('yarn -v'),
|
|
24
|
+
// 3. 当前生效的镜像源
|
|
25
|
+
registry: {
|
|
26
|
+
npm: getSafeExec('npm config get registry'),
|
|
27
|
+
pnpm: getSafeExec('pnpm config get registry'),
|
|
28
|
+
yarn: getSafeExec('yarn config get registry')
|
|
29
|
+
},
|
|
30
|
+
// 4. 锁定文件检测(判断实际安装方式)
|
|
31
|
+
lockfile: {
|
|
32
|
+
hasPackageLock: fs.existsSync(path.join(process.cwd(), 'package-lock.json')),
|
|
33
|
+
hasPnpmLock: fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml')),
|
|
34
|
+
hasYarnLock: fs.existsSync(path.join(process.cwd(), 'yarn.lock'))
|
|
35
|
+
},
|
|
36
|
+
// 5. 核心依赖版本 (可选)
|
|
37
|
+
coreDeps: {}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// 自动识别当前项目使用的包管理器
|
|
41
|
+
info.activeManager = info.lockfile.hasPnpmLock ? 'pnpm' :
|
|
42
|
+
info.lockfile.hasYarnLock ? 'yarn' : 'npm';
|
|
43
|
+
|
|
44
|
+
const logPath = path.join(process.cwd(), '.deplens.history.json');
|
|
45
|
+
|
|
46
|
+
// 维护一个历史记录列表(保留最近10次安装记录)
|
|
47
|
+
let history = [];
|
|
48
|
+
if (fs.existsSync(logPath)) {
|
|
49
|
+
try { history = JSON.parse(fs.readFileSync(logPath)); } catch (e) { }
|
|
50
|
+
}
|
|
51
|
+
history.unshift(info);
|
|
52
|
+
fs.writeFileSync(logPath, JSON.stringify(history.slice(0, 10), null, 2));
|
|
53
|
+
|
|
54
|
+
return logPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
export default recordEnv;
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import recordEnv from './env-recorder.js';
|
|
4
|
+
import startServer from './diffView.js';
|
|
5
|
+
import { packageManagers } from './config.js';
|
|
6
|
+
import { checkEnv, autoInstall, initProject } from './env-checker.js';
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const command = args[0];
|
|
9
|
+
const packageJson = await import('../package.json', { with: { type: 'json' } });
|
|
10
|
+
const version = packageJson.default.version;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const tip = `
|
|
14
|
+
用法: depLens [选项]
|
|
15
|
+
|
|
16
|
+
选项:
|
|
17
|
+
-init 初始化项目标准
|
|
18
|
+
-track 记录当前 Node/npm/镜像 等环境信息
|
|
19
|
+
-view 启动本地 Web 服务查看依赖空间镜像
|
|
20
|
+
-check 检查当前环境是否符合记录的环境标准
|
|
21
|
+
-install 自动安装项目依赖
|
|
22
|
+
-package -p 查看支持的包管理器
|
|
23
|
+
-h 显示帮助信息
|
|
24
|
+
-v 查看版本
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
switch (command) {
|
|
29
|
+
// 初始化项目标准
|
|
30
|
+
case '-init':
|
|
31
|
+
await initProject();
|
|
32
|
+
break;
|
|
33
|
+
|
|
34
|
+
// 记录当前环境快照
|
|
35
|
+
case '-track':
|
|
36
|
+
case '-t':
|
|
37
|
+
case 'track':
|
|
38
|
+
console.log('🚀 正在采集当前环境快照...');
|
|
39
|
+
const logPath = recordEnv();
|
|
40
|
+
console.log(`✅ 环境已记录至: ${logPath}`);
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
// 启动本地 Web 服务查看依赖空间镜像
|
|
44
|
+
case '-view':
|
|
45
|
+
case 'view':
|
|
46
|
+
console.log('🌐 正在启动 DepLens 可视化界面...');
|
|
47
|
+
await startServer(); // 启动你之前的 D3 渲染逻辑
|
|
48
|
+
break;
|
|
49
|
+
|
|
50
|
+
// 检查当前环境是否符合记录的环境标准
|
|
51
|
+
case '-check':
|
|
52
|
+
await checkEnv();
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
// 自动安装项目依赖
|
|
56
|
+
case '-install':
|
|
57
|
+
await autoInstall();
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
// 查看版本
|
|
61
|
+
case '--version':
|
|
62
|
+
case '-v':
|
|
63
|
+
console.log(`DepLens v${version}`);
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
// 查看支持的包管理器
|
|
67
|
+
case '-package':
|
|
68
|
+
case '-p':
|
|
69
|
+
console.log('✅ DepLens 支持的包管理器:', packageManagers);
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
// 显示帮助信息
|
|
73
|
+
case '-h':
|
|
74
|
+
console.log(tip);
|
|
75
|
+
process.exit(0);
|
|
76
|
+
|
|
77
|
+
// 默认显示帮助信息
|
|
78
|
+
default:
|
|
79
|
+
console.log(tip);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main();
|