简易 node_modules 补丁管理
- 🛠 增量式补丁应用
- 🧪 试运行模式(dry-run)
- 📦 自动空目录清理
安装
npm install fs-extra chalk # 安装依赖
参数说明
参数 | 描述 |
---|---|
--dry-run | 试运行模式(仅显示操作预览) |
--patches-dir | 指定补丁目录(默认:custom_modules ) |
--node-modules-dir | 指定目标node_modules 目录(默认:node_modules ) |
清单文件
文件路径:node_modules/.patch-manifest.json
作用:记录当前生效的补丁文件路径,用于后续清理操作
文件删除后自动执行
从文件所在目录向上递归检查
删除所有空目录直至node_modules根目录
跳过包含其他文件的目录
删除操作会触发空目录清理
示例
package.json 添加
“patches”: “node scripts/apply-patches.js --dry-run”
npm run patches
代码部分
根目录创建 scripts/apply-patches.js
// 用于对node_modules进行定制化修改
/**
* 模块依赖:
* fs-extra: 增强版文件系统模块,支持Promise和额外方法
* path: 路径处理模块
* chalk: 终端字符串样式库
*/
const fs = require("fs-extra");
const path = require("path");
const chalk = require("chalk");
/* 命令行参数解析 */
const args = process.argv.slice(2); // 获取命令行参数(跳过前两个Node执行路径参数)
const isDryRun = args.includes("--dry-run"); // 检查是否启用试运行模式(不实际修改文件)
const getArg = (name) => args.includes(name) ? args[args.indexOf(name) + 1] : null; // 获取指定参数值的工具函数
/* 配置路径处理 */
const BASE_DIR = path.resolve(__dirname, ".."); // 项目根目录(脚本所在目录的上级)
const PATCHES_DIR = path.resolve(BASE_DIR,getArg("--patches-dir") || "custom_modules"); // 自定义补丁目录(默认为custom_modules)
const NODE_MODULES_DIR = path.resolve(BASE_DIR,getArg("--node-modules-dir") || "node_modules");// 目标node_modules目录
const MANIFEST_FILE = path.join(NODE_MODULES_DIR, ".patch-manifest.json"); // 补丁清单文件路径
/**
* 清理空目录(从文件路径向上递归清理直到node_modules根目录)
* @param {string} filePath - 被删除文件的路径,用于确定清理起点
*/
function cleanEmptyDirectories(filePath) {
let currentDir = path.dirname(filePath); // 从文件所在目录开始检查
while (currentDir !== NODE_MODULES_DIR) { // 不超出node_modules范围
if (!fs.existsSync(currentDir)) break; // 目录不存在则终止
const contents = fs.readdirSync(currentDir);
if (contents.length > 0) break; // 目录非空则终止
try {
// 执行目录删除(dry-run模式跳过)
if (!isDryRun) {
fs.rmdirSync(currentDir);
console.log(chalk.yellow(`清理空目录: ${path.relative(NODE_MODULES_DIR, currentDir)}`));
} else {
console.log(chalk.yellow(`[dry-run] 将清理空目录: ${path.relative(NODE_MODULES_DIR, currentDir)}`));
}
currentDir = path.dirname(currentDir); // 继续检查上级目录
} catch (e) {
console.error(chalk.red(`目录清理失败: ${currentDir}`), e.message);
break;
}
}
}
/**
* 补丁应用主函数
* @returns {Object} 包含各操作类型数量的统计对象
*/
function applyPatches() {
let manifest = []; // 当前补丁文件清单(相对路径集合)
let operations = { created: 0, updated: 0, deleted: 0 }; // 操作计数器
/**
* 阶段1:应用新补丁(递归处理补丁目录)
* @param {string} srcDir - 当前处理的补丁源目录
* @param {string} targetDir - 对应的目标node_modules目录
*/
function processDirectory(srcDir, targetDir) {
if (!fs.existsSync(srcDir)) return;
// 遍历补丁目录(过滤隐藏文件和node_modules)
fs.readdirSync(srcDir)
.filter(file => !file.startsWith(".") && file !== "node_modules")
.forEach(file => {
const srcPath = path.join(srcDir, file);
const targetPath = path.join(targetDir, file);
const relativePath = path.relative(NODE_MODULES_DIR, targetPath);
// 处理目录类型
if (fs.statSync(srcPath).isDirectory()) {
if (!isDryRun) fs.ensureDirSync(targetPath); // 确保目标目录存在
processDirectory(srcPath, targetPath); // 递归处理子目录
}
// 处理文件类型
else {
manifest.push(relativePath); // 记录到当前清单
let operation = "skipped"; // 默认操作状态
try {
const srcContent = fs.readFileSync(srcPath, "utf8");
const targetExists = fs.existsSync(targetPath);
// 判断是否需要更新或创建
if (!targetExists || srcContent !== fs.readFileSync(targetPath, "utf8")) {
operation = targetExists ? "updated" : "created";
if (!isDryRun) {
fs.copyFileSync(srcPath, targetPath); // 执行文件复制
operations[operation]++; // 更新计数器
}
}
} catch (e) {
console.error(chalk.red(`处理失败: ${relativePath}`), e.message);
}
// 根据操作类型输出彩色日志
const color = { created: "green", updated: "cyan", skipped: "gray" }[operation];
console.log(chalk[color](`[${operation}] ${relativePath}`));
}
});
}
/**
* 阶段2:清理旧文件(对比新旧清单删除多余文件)
*/
function cleanObsoleteFiles() {
// 读取旧清单(不存在则返回空数组)
const oldManifest = fs.existsSync(MANIFEST_FILE)
? fs.readJsonSync(MANIFEST_FILE)
: [];
// 找出需要删除的旧文件路径
oldManifest
.filter(relPath => !manifest.includes(relPath))
.forEach(relPath => {
const targetPath = path.join(NODE_MODULES_DIR, relPath);
if (fs.existsSync(targetPath)) {
try {
if (!isDryRun) {
fs.removeSync(targetPath); // 删除文件
operations.deleted++; // 更新计数器
cleanEmptyDirectories(targetPath); // 清理空目录
}
console.log(chalk.red(`[deleted] ${relPath}`));
} catch (e) {
console.error(chalk.red(`删除失败: ${relPath}`), e.message);
}
}
});
}
/* 执行处理流程 */
console.log(chalk.blue.bold("\n=== 补丁管理 ==="));
console.log(`模式: ${chalk.yellow(isDryRun ? "DRY-RUN" : "实际执行")}`);
console.log(`补丁目录: ${chalk.cyan(PATCHES_DIR)}`);
console.log(`目标目录: ${chalk.cyan(NODE_MODULES_DIR)}\n`);
// 执行两个处理阶段
processDirectory(PATCHES_DIR, NODE_MODULES_DIR);
cleanObsoleteFiles();
// 保存新清单文件(dry-run模式跳过)
if (!isDryRun) {
fs.writeJsonSync(MANIFEST_FILE, manifest.sort(), { spaces: 2 });
console.log(chalk.gray("清单文件已更新"));
}
return operations;
}
/* 主程序执行 */
try {
const { created, updated, deleted } = applyPatches();
console.log(chalk.blue.bold("\n=== 执行结果 ==="));
console.log(`新增文件: ${chalk.green(created)}`);
console.log(`更新文件: ${chalk.cyan(updated)}`);
console.log(`删除文件: ${chalk.red(deleted)}`);
if (isDryRun) {
console.log(chalk.yellow.bold("\n注意:dry-run模式未实际修改文件"));
}
} catch (e) {
console.error(chalk.red.bold("\n错误终止:"), e.message);
process.exit(1);
}
package.json
"scripts": {
"patches": "node scripts/apply-patches.js",
"start": "npm run dev"
},
实机效果