🚀 神器!一键导出 node_modules 为 .tgz 包,解决依赖管理痛点
前言
在前端开发中,你是否遇到过以下痛点?
- 内网环境无法访问 npm 仓库,依赖安装困难
- 项目迁移时需要重新安装大量依赖,耗时耗力
- 依赖版本管理混乱,不同环境可能安装不同版本
- 离线部署时需要手动收集所有依赖包
别担心,今天我将介绍一个我开发的 Node.js 工具 —— export-node-modules-tgz.js,它可以一键导出 node_modules 中的所有包及其依赖为 .tgz 文件,完美解决上述问题!
工具简介
export-node-modules-tgz.js 是一个基于 Node.js 的命令行工具,主要功能包括:
- ✅ 导出所有顶级依赖及其递归依赖
- ✅ 支持导出特定包及其依赖
- ✅ 自动创建输出目录
- ✅ 智能解析包版本信息
- ✅ 生成详细的打包报告
- ✅ 处理作用域包(如 @babel/core)
核心功能实现
1. 依赖收集机制
工具的核心是 collectDependencies 函数,它采用递归方式收集所有依赖:
function collectDependencies(packageName, collected) {
const normalizedPackageName = packageName.replace(/\\/g, "/");
const version = getExactVersion(normalizedPackageName);
if (version) {
const packageId = `${normalizedPackageName}@${version}`;
if (!collected.has(packageId)) {
collected.add(packageId);
// 从 package.json 读取依赖并递归收集
const packageJsonPath = path.join(
NODE_MODULES_DIR,
normalizedPackageName,
"package.json",
);
try {
if (fs.existsSync(packageJsonPath)) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
if (pkg.dependencies) {
Object.keys(pkg.dependencies).forEach((depName) => {
collectDependencies(depName, collected);
});
}
}
} catch (error) {
console.warn(
`⚠️ 读取 ${packageName} 的依赖失败: ${error.message}`,
);
}
}
}
}
这个函数的工作原理是:
- 接收包名和已收集的包集合
- 标准化包名(处理路径分隔符)
- 获取包的精确版本
- 生成唯一的包 ID(包名@版本)
- 递归收集该包的所有依赖
2. 顶级包扫描
getTopLevelPackages 函数用于扫描 node_modules 中的所有顶级包:
function getTopLevelPackages() {
const packages = [];
const items = fs.readdirSync(NODE_MODULES_DIR, { withFileTypes: true });
items.forEach((item) => {
if (item.isDirectory()) {
const itemName = item.name;
// 跳过 .bin 目录
if (itemName === ".bin") {
return;
}
if (itemName.startsWith("@")) {
// 作用域包目录
const scopePath = path.join(NODE_MODULES_DIR, itemName);
const scopeItems = fs.readdirSync(scopePath, { withFileTypes: true });
scopeItems.forEach((scopeItem) => {
if (scopeItem.isDirectory()) {
const scopedPackageName = `${itemName}/${scopeItem.name}`;
packages.push(scopedPackageName);
}
});
} else {
// 普通包
packages.push(itemName);
}
}
});
return packages;
}
这个函数会:
- 遍历 node_modules 目录
- 跳过 .bin 目录
- 处理作用域包(如 @babel/core)
- 收集所有顶级包名
3. 包打包机制
packPackage 函数使用 npm pack 命令打包包:
function packPackage(packageId) {
try {
console.log(`\n处理中: ${packageId}`);
execSync(`npm pack ${packageId} --pack-destination ${OUTPUT_DIR}`, {
stdio: "pipe",
stderr: "pipe",
});
console.log(`✅ 成功: ${packageId}`);
return true;
} catch (error) {
console.error(`❌ 失败: ${packageId}`);
console.error(` 错误: ${error.stderr.toString().trim()}`);
return false;
}
}
这个函数会:
- 执行 npm pack 命令
- 指定打包目标目录
- 捕获并处理错误
- 返回打包结果
使用方法
1. 导出所有包
node export-node-modules-tgz.js
2. 导出特定包
node export-node-modules-tgz.js lodash axios @babel/core
3. 查看输出
执行命令后,工具会:
- 扫描 node_modules 中的包
- 递归收集所有依赖
- 打包所有包到
node_modules-tgz目录 - 生成详细的打包报告
实际应用场景
场景一:内网环境部署
- 在有网络的环境中运行工具导出所有依赖
- 将
node_modules-tgz目录复制到内网环境 - 使用
npm install *.tgz命令安装所有依赖
场景二:项目迁移
- 导出当前项目的所有依赖
- 在新环境中解压导出的包
- 执行安装命令,快速搭建相同的依赖环境
场景三:依赖版本锁定
- 导出特定版本的依赖包
- 将这些包纳入版本控制
- 确保所有环境使用相同版本的依赖
代码优化建议
-
并行打包:当前实现是串行打包,可以使用
Promise.all并行处理,提高打包速度 -
缓存机制:添加缓存机制,避免重复打包相同的包
-
配置文件:支持通过配置文件指定导出选项
-
进度条:添加打包进度条,提升用户体验
-
错误重试:对失败的包添加自动重试机制
完整代码
/**
* 将 node_modules 包及其依赖导出为 .tgz 文件
* 使用 Node.js 处理此过程
*
* 使用方法:
* # 导出所有包
* node export-node-modules-tgz.js
*
* # 导出特定包
* node export-node-modules-tgz.js 包名1 包名2 @作用域/包名3
*/
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
const OUTPUT_DIR = "./node_modules-tgz";
const NODE_MODULES_DIR = "./node_modules";
// 解析命令行参数以获取要导出的特定包
const args = process.argv.slice(2);
const specificPackages = args.length > 0 ? args : null;
if (specificPackages) {
console.log(`📋 导出特定包: ${specificPackages.join(", ")}`);
} else {
console.log("📋 导出所有包");
}
// 如果输出目录不存在则创建
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
console.log(`✅ 输出目录已创建: ${OUTPUT_DIR}`);
}
// 检查 node_modules 是否存在
if (!fs.existsSync(NODE_MODULES_DIR)) {
console.error(
"❌ 未找到 node_modules 目录!请先运行 `npm install`。",
);
process.exit(1);
}
/**
* 从 package.json 获取包的精确版本
* @param {string} packageName - 包名(例如 "lodash" 或 "@ant-design/colors")
* @returns {string|null} - 精确版本或未找到时返回 null
*/
function getExactVersion(packageName) {
const packageJsonPath = path.join(
NODE_MODULES_DIR,
packageName,
"package.json",
);
try {
if (fs.existsSync(packageJsonPath)) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
return pkg.version || null;
}
} catch (error) {
console.warn(
`⚠️ 读取 ${packageName} 的 package.json 失败: ${error.message}`,
);
}
return null;
}
/**
* 递归收集所有依赖
* @param {string} packageName - 包名
* @param {Set} collected - 收集的包 ID 集合
*/
function collectDependencies(packageName, collected) {
const normalizedPackageName = packageName.replace(/\\/g, "/");
const version = getExactVersion(normalizedPackageName);
if (version) {
const packageId = `${normalizedPackageName}@${version}`;
if (!collected.has(packageId)) {
collected.add(packageId);
// 从 package.json 读取依赖并递归收集
const packageJsonPath = path.join(
NODE_MODULES_DIR,
normalizedPackageName,
"package.json",
);
try {
if (fs.existsSync(packageJsonPath)) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
if (pkg.dependencies) {
Object.keys(pkg.dependencies).forEach((depName) => {
collectDependencies(depName, collected);
});
}
}
} catch (error) {
console.warn(
`⚠️ 读取 ${packageName} 的依赖失败: ${error.message}`,
);
}
}
}
}
/**
* 获取 node_modules 中的所有顶级包
* @returns {string[]} - 顶级包名数组
*/
function getTopLevelPackages() {
const packages = [];
const items = fs.readdirSync(NODE_MODULES_DIR, { withFileTypes: true });
items.forEach((item) => {
if (item.isDirectory()) {
const itemName = item.name;
// 跳过 .bin 目录
if (itemName === ".bin") {
return;
}
if (itemName.startsWith("@")) {
// 作用域包目录
const scopePath = path.join(NODE_MODULES_DIR, itemName);
const scopeItems = fs.readdirSync(scopePath, { withFileTypes: true });
scopeItems.forEach((scopeItem) => {
if (scopeItem.isDirectory()) {
const scopedPackageName = `${itemName}/${scopeItem.name}`;
packages.push(scopedPackageName);
console.log(` 发现作用域包: ${scopedPackageName}`);
}
});
} else {
// 普通包
packages.push(itemName);
console.log(` 发现包: ${itemName}`);
}
}
});
return packages;
}
/**
* 使用 npm pack 打包包
* @param {string} packageId - 包 ID,格式为 "名称@版本"
* @returns {boolean} - 成功返回 true,失败返回 false
*/
function packPackage(packageId) {
try {
console.log(`\n处理中: ${packageId}`);
execSync(`npm pack ${packageId} --pack-destination ${OUTPUT_DIR}`, {
stdio: "pipe",
stderr: "pipe",
});
console.log(`✅ 成功: ${packageId}`);
return true;
} catch (error) {
console.error(`❌ 失败: ${packageId}`);
console.error(` 错误: ${error.stderr.toString().trim()}`);
return false;
}
}
// 主函数
function main() {
console.log("🔍 扫描 node_modules 中的包...");
let targetPackages;
if (specificPackages) {
// 使用命令行指定的包
targetPackages = specificPackages;
console.log(`✅ 使用 ${targetPackages.length} 个指定的包`);
} else {
// 获取所有顶级包
targetPackages = getTopLevelPackages();
console.log(`✅ 发现 ${targetPackages.length} 个顶级包`);
}
// 递归收集所有依赖
console.log("\n🔗 递归收集所有依赖...");
const collectedPackages = new Set();
targetPackages.forEach((pkgName) => {
collectDependencies(pkgName, collectedPackages);
});
const uniquePackages = Array.from(collectedPackages).sort();
console.log(`✅ 发现 ${uniquePackages.length} 个唯一依赖`);
// 打包所有包
console.log("\n📦 开始打包...");
let successCount = 0;
let failCount = 0;
const failedPackages = [];
uniquePackages.forEach((pkgId, index) => {
console.log(`\n[${index + 1}/${uniquePackages.length}]`);
const success = packPackage(pkgId);
if (success) {
successCount++;
} else {
failCount++;
failedPackages.push(pkgId);
}
});
// 最终报告
console.log("\n==================================================");
console.log("📊 打包报告");
console.log("==================================================");
console.log(`✅ 成功: ${successCount} 个`);
console.log(`❌ 失败: ${failCount} 个`);
if (failedPackages.length > 0) {
console.log("⚠️ 失败列表:");
failedPackages.forEach((pkg) => console.log(` - ${pkg}`));
}
console.log(`📁 输出路径: ${path.resolve(OUTPUT_DIR)}`);
console.log("==================================================");
}
// 运行主函数
main();
最终效果
总结
export-node-modules-tgz.js 是一个简单但强大的工具,它可以帮助开发者轻松解决依赖管理中的各种痛点。通过一键导出 node_modules 为 .tgz 包,我们可以:
- 简化内网环境部署
- 加速项目迁移
- 确保依赖版本一致性
- 实现离线环境的依赖安装
希望这个工具能为你的开发工作带来便利!如果你有任何问题或建议,欢迎在评论区留言讨论。
相关推荐: