自动化npm包批量对比和同步:解决内网npm源同步问题(CLI)
背景
在使用 @sxf/i18n-next-tool 做流水线的静态检查时,我们小组的人遇到了一个棘手的问题:该工具没有锁定 @babel/plugin- 相关包的小版本。当在内网环境中安装时,镜像源发现了新版本,但这些新版本并未同步到内网镜像源中,根据排查发现有几十个包出了最新版本,需要同步更新。

问题分析
手动同步几十个包是一项繁琐且容易出错的工作。

要反复执行以下繁琐且低效率的流程:
- 执行安装
npm i -g @sxf/i18n-next-tool - 安装失败,发现有最新包没同步
- 去 npm 源网站更新安装失败的包的最新版,等待同步成功
- 循环执行安装
npm i -g @sxf/i18n-next-tool这个操作。失败了回到第二步同步下一个包 - 直到安装成功,重新运行流水线
这个过程非常耗时且容易出错,因此我决定编写一个自动化脚本来解决这个问题。
解决方案
编写一个自动化脚本来解决这个问题。以下是我解决过程的关键步骤:
- 获取包列表
首先,我实现了一个搜索函数来获取所有 @babel/plugin- 相关的包:
/**
* 搜索包的函数
* @param searchText 搜索文本
* @returns Promise<Package[]> 包列表
*/
async function searchPackages(searchText: string): Promise<Package[]> {
const encodedSearchText = encodeURIComponent(searchText);
// 首先获取总数
const initialOptions: http.RequestOptions = {
hostname: REGISTRY_URL,
port: 80,
path: `${SEARCH_PATH.replace('{text}', encodedSearchText).replace(
'{size}',
'5'
)}`,
method: 'GET',
headers: {
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
},
};
const initialResponse = await makeRequest(initialOptions);
const initialResult: SearchResponse = JSON.parse(initialResponse);
const totalPackages = initialResult.total;
// 使用总数作为 size 再次请求
const fullOptions: http.RequestOptions = {
...initialOptions,
path: `${SEARCH_PATH.replace('{text}', encodedSearchText).replace(
'{size}',
totalPackages.toString()
)}`,
};
const fullResponse = await makeRequest(fullOptions);
const fullResult: SearchResponse = JSON.parse(fullResponse);
return fullResult.objects.map((obj) => {
return {
name: obj.package.name,
};
});
}- 对比版本
然后,我编写了两个函数来分别获取远程和本地的包信息:
/**
* 获取远程最新的包信息
* @param packageName 包名
* @returns Promise<any | null> 远程包信息,如果包不存在则返回 null
*/
async function getRemotePackageInfo(packageName: string): Promise<any | null> {
const options: http.RequestOptions = {
hostname: REGISTRY_URL,
port: 80,
path: `/-/remote/${encodeURIComponent(packageName)}`,
method: 'GET',
headers: {
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Referer': `http://${REGISTRY_URL}/sync.html`,
'Referrer-Policy': 'strict-origin-when-cross-origin',
},
};
try {
const response = await makeRequest(options);
const parsedResponse = JSON.parse(response);
if (parsedResponse.error && parsedResponse.error.includes('NOT_FOUND')) {
console.log(`包 ${packageName} 在远程源中不存在`);
return null;
}
return parsedResponse;
} catch (error) {
console.error(`获取远程包 ${packageName} 信息时发生错误:`, error);
throw error;
}
}
/**
* 获取当前私有源上的包信息
* @param packageName 包名
* @returns Promise<any> 当前私有源上的包信息
*/
async function getLocalPackageInfo(packageName: string): Promise<any> {
const options: http.RequestOptions = {
hostname: REGISTRY_URL,
port: 80,
path: `/${encodeURIComponent(packageName)}`,
method: 'GET',
headers: {
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
},
};
try {
const response = await makeRequest(options);
return JSON.parse(response);
} catch (error) {
console.error(`获取本地包 ${packageName} 信息时发生错误:`, error);
throw error;
}
}- 单个同步和批量同步
最后,我实现了同步函数和主控制流程:
/**
* 同步单个包的函数
* @param packageName 包名
*/
async function syncPackage(packageName: string): Promise<void> {
const options: http.RequestOptions = {
hostname: REGISTRY_URL,
port: 80,
path: SYNC_PATH.replace('{package}', encodeURIComponent(packageName)),
method: 'PUT',
headers: {
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'content-type': 'application/x-www-form-urlencoded',
'Referer': `http://${REGISTRY_URL}/sync.html`,
'Referrer-Policy': 'strict-origin-when-cross-origin',
},
};
try {
await makeRequest(options);
console.log(`已同步包: ${packageName}`);
} catch (error) {
console.error(`同步包 ${packageName} 时发生错误:`, error);
throw error;
}
}
/**
* 主函数,协调整个同步过程
*/
async function main(text: string, verbose: boolean = false) {
try {
console.log(`\n📦 开始搜索包含 "${text}" 的包...\n`);
const packages = await searchPackages(text);
console.log(`✅ 找到 ${packages.length} 个包\n`);
for (let i = 0; i < packages.length; i++) {
const pkg = packages[i];
console.log(`[${i + 1}/${packages.length}] 处理包: ${pkg.name}`);
try {
const remoteInfo = await getRemotePackageInfo(pkg.name);
if (remoteInfo === null) {
console.log(` ℹ️ 包 ${pkg.name} 只存在于内网私有源,无需同步\n`);
continue;
}
const localInfo = await getLocalPackageInfo(pkg.name);
const remoteVersion = remoteInfo['dist-tags']?.latest;
const localVersion = localInfo['dist-tags']?.latest;
if (verbose) {
console.log(` 📡 远程最新版本: ${remoteVersion}`);
console.log(` 🏠 本地当前版本: ${localVersion}`);
}
if (remoteVersion !== localVersion) {
console.log(` 🔄 正在发起同步...`);
await syncPackage(pkg.name);
console.log(` ✅ 同步完成\n`);
} else {
if (verbose) {
console.log(` ✅ 已是最新版本,无需同步\n`);
}
}
} catch (error) {
console.error(` ❌ 处理包 ${pkg.name} 时发生错误:`, error);
console.log(` ⏭️ 继续处理下一个包...\n`);
}
}
console.log(`✅ 所有 "${text}" 包的同步过程已完成\n`);
} catch (error) {
console.error('❌ 同步过程中发生错误:', error);
}
}
// 导出主函数
export { main };- CLI 工具封装
为了使这个脚本更易于使用,我将其封装成了一个 CLI 工具并发布在内网源中。
以下是 CLI 入口文件的代码:
#!/usr/bin/env node
import { program } from 'commander';
import { main } from './sync_npm_package';
program
.version('1.0.0')
.description('一个用于同步 npm 包的 CLI 工具')
.argument('<search-text>', '要搜索的包名文本')
.option('-v, --verbose', '显示详细输出')
.action((searchText: string, options: { verbose: boolean }) => {
main(searchText, options.verbose);
});
program.parse(process.argv);- 运行效果

使用方法
文档地址: http://npm.uedc.sangfor.com.cn/sync.html#@sxf/npm-sync-cli
通过 npx 临时调用:
npx npm-sync <search-text>全局安装:
npm install -g @sxf/npm-sync-cli使用:
npm-sync <search-text>选项:
- -v, —verbose: 显示详细输出
示例:
npm-sync "@babel/plugin-" -v这个命令会搜索并同步所有包含”@babel/plugin-”的包,并显示详细输出。
技巧与经验
在解决这个问题的过程中,我运用了以下技巧:
- 利用浏览器开发工具:从浏览器的 Network 面板复制出 fetch 请求,然后将其转换为 Node.js 代码。
- 借助 AI 辅助编程:使用 Claude 等 AI 工具帮助转换和优化代码。
- 渐进式开发:从基本功能开始,逐步添加更复杂的逻辑,最终满足所有需求。
- 模块化设计:将不同功能拆分为独立的函数,提高代码的可读性和可维护性。
- CLI 工具封装: 将脚本封装成 CLI 工具,提高使用便利性和通用性。
心得体会
通过这次经历,我深刻体会到了自动化的重要性。用自动化脚本代替重复的人工工作不仅提高了效率,还大大降低了出错的可能性。将脚本封装成 CLI 工具更是提高了其通用性和易用性,使得团队中的其他成员也能方便地使用这个工具。这再次证明了”懒惰是程序员的美德”这句话的正确性。
启发与帮助
这个解决方案对其他开发者有以下启发:
- 通用性:这个脚本是通用的,可以用
ts-node直接运行,适用于任何需要批量同步 npm 包的场景。 - 问题解决思路:面对复杂问题,可以将其拆解为小步骤,逐个击破。
- 工具的灵活运用:善用浏览器开发工具、AI 辅助工具等,可以大大提高开发效率。
- 自动化思维:对于重复性的工作,要养成编写自动化脚本的习惯。
- 易用性思维:相比于提供脚本代码给别人使用,更好的做法事将脚本封装成 CLI 工具,提高使用便利性和通用性。
结论
通过这个项目,我不仅解决了当前的同步问题,还创造了一个可以长期使用的工具。它提醒我,在日常开发中,我应该时刻思考如何通过自动化来提高效率,减少人为错误。希望这个经验能够帮助到其他面临类似挑战的开发者。
价值

