node_modules进行定制化修改 打补丁

简易 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"
  },
实机效果

新增文件的时候
删除文件的时候

撤🚗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值