pacote-install
Version:
A tool to download npm packages from specific registries
392 lines (391 loc) • 19.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.installPackage = installPackage;
exports.installDependencies = installDependencies;
exports.install = install;
const pacote_1 = __importDefault(require("pacote"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const logger_1 = __importDefault(require("./utils/logger"));
const retry_1 = __importDefault(require("./utils/retry"));
const json_1 = require("./utils/json");
// 默认选项
const DEFAULT_OPTIONS = {
registry: 'https://registry.npmjs.org/',
retries: 3,
timeout: 60000,
verbose: false,
silent: false,
concurrency: 5,
peer: false,
force: false
};
/**
* 安装npm包到指定目录
* @param packageName 包名称,例如 @ub/bell-cli 或 @ub/bell-cli@latest
* @param targetDir 目标安装目录
* @param options 选项
* @returns Promise
*/
async function installPackage(packageName, targetDir, options = {}) {
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
// 设置日志级别
if (mergedOptions.verbose) {
logger_1.default.setLevel(0); // DEBUG
}
else if (mergedOptions.silent) {
logger_1.default.setLevel(4); // SILENT
}
logger_1.default.debug(`开始安装包 ${packageName} 到 ${targetDir}`);
logger_1.default.debug(`使用选项: ${JSON.stringify(mergedOptions)}`);
try {
// 确保目标目录存在
fs_1.default.mkdirSync(targetDir, { recursive: true });
logger_1.default.debug(`已创建目标目录: ${targetDir}`);
// 尝试获取包信息
logger_1.default.info(`正在获取包信息: ${packageName}`);
const manifest = await (0, retry_1.default)(() => pacote_1.default.manifest(packageName, {
registry: mergedOptions.registry,
timeout: mergedOptions.timeout
}), {
retries: mergedOptions.retries,
onRetry: (error, attempt) => {
logger_1.default.warn(`获取包信息失败 (${attempt}/${mergedOptions.retries}): ${error.message}`);
}
});
logger_1.default.debug(`包信息获取成功: ${manifest.name}@${manifest.version}`);
// 当启用force选项时,检查目标目录是否已存在包
if (mergedOptions.force && fs_1.default.existsSync(path_1.default.join(targetDir, 'package.json'))) {
try {
const existingPackageJson = (0, json_1.jsonParse)(fs_1.default.readFileSync(path_1.default.join(targetDir, 'package.json'), 'utf-8'));
const existingVersion = String(existingPackageJson.version || '');
const requestedVersion = manifest.version;
if (existingPackageJson.name === manifest.name) {
logger_1.default.info(`发现目标目录已存在 ${existingPackageJson.name}@${existingVersion}`);
if (existingVersion !== requestedVersion) {
logger_1.default.warn(`使用 force 模式,保留已安装的版本 ${existingVersion},而不是请求的版本 ${requestedVersion}`);
}
else {
logger_1.default.info(`已安装版本 ${existingVersion} 与请求版本相同,跳过安装`);
}
return targetDir;
}
}
catch (error) {
logger_1.default.debug(`读取目标目录中的 package.json 失败,将继续安装`);
}
}
// 下载并解压到指定目录
logger_1.default.info(`正在下载并解压包: ${packageName} => ${targetDir}`);
await (0, retry_1.default)(() => pacote_1.default.extract(packageName, targetDir, {
registry: mergedOptions.registry,
timeout: mergedOptions.timeout
}), {
retries: mergedOptions.retries,
onRetry: (error, attempt) => {
logger_1.default.warn(`解压包失败 ${manifest.name} (${attempt}/${mergedOptions.retries}): ${error.message}`);
},
beforeFinish: (res, attempt) => {
const packageJson = (0, json_1.jsonParse)(fs_1.default.readFileSync(path_1.default.join(targetDir, 'package.json'), 'utf-8'));
logger_1.default.info(`检查包信息 ${manifest.name},第 ${attempt + 1} 次结束检查,结果: ${packageJson.name}@${packageJson.version} 成功`);
}
});
logger_1.default.success(`包安装成功: ${manifest.name}@${manifest.version}`);
return targetDir;
}
catch (error) {
const enhancedError = logger_1.default.wrapError(error, `安装包 ${packageName} 到 ${targetDir} 失败`);
logger_1.default.error(enhancedError);
throw enhancedError;
}
}
/**
* 递归安装依赖
* @param packageDir 包目录
* @param options 选项
*/
async function installDependencies(packageDir, options = {}) {
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
const packageJsonPath = path_1.default.join(packageDir, 'package.json');
logger_1.default.debug(`检查依赖: ${packageJsonPath}`);
if (!fs_1.default.existsSync(packageJsonPath)) {
logger_1.default.debug(`未找到 package.json: ${packageJsonPath}`);
return;
}
try {
// 读取package.json
const packageJson = (0, json_1.jsonParse)(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
const dependencies = packageJson.dependencies || {};
// 如果启用了peer选项,将peerDependencies合并到要安装的依赖中
let combinedDependencies = { ...dependencies };
if (mergedOptions.peer && packageJson.peerDependencies) {
logger_1.default.info(`启用peer选项,将安装peerDependencies`);
logger_1.default.debug(`peerDependencies: ${JSON.stringify(packageJson.peerDependencies)}`);
combinedDependencies = { ...combinedDependencies, ...packageJson.peerDependencies };
}
const dependencyCount = Object.keys(combinedDependencies).length;
if (dependencyCount === 0) {
logger_1.default.info(`包 ${path_1.default.basename(packageDir)} 没有依赖项`);
return;
}
logger_1.default.info(`正在安装 ${path_1.default.basename(packageDir)} 的 ${dependencyCount} 个依赖`);
// 获取当前工作目录的node_modules路径
const cwdNodeModulesDir = path_1.default.join(process.cwd(), 'node_modules');
// 确保工作目录的node_modules存在
fs_1.default.mkdirSync(cwdNodeModulesDir, { recursive: true });
// 创建包目录下的node_modules目录(作为备选安装位置)
const packageNodeModulesDir = path_1.default.join(packageDir, 'node_modules');
fs_1.default.mkdirSync(packageNodeModulesDir, { recursive: true });
// 创建进度条
logger_1.default.createProgressBar(dependencyCount, `安装依赖`);
// 限制并发数
const queue = Object.entries(combinedDependencies);
const results = [];
let completed = 0;
// 并发控制的安装函数
const installConcurrently = async () => {
while (queue.length > 0) {
const [name, version] = queue.shift();
try {
const depPackageName = `${name}@${version}`;
// 优先使用工作目录下的node_modules
const depCwdTargetDir = path_1.default.join(cwdNodeModulesDir, name);
// 包目录下的node_modules作为备选
const depPackageTargetDir = path_1.default.join(packageNodeModulesDir, name);
logger_1.default.debug(`开始安装依赖: ${depPackageName}`);
// 检查当前工作目录的node_modules中是否已安装
const isCwdInstalled = fs_1.default.existsSync(depCwdTargetDir) && fs_1.default.existsSync(path_1.default.join(depCwdTargetDir, 'package.json'));
let isVersionMatch = false;
let installedVersion = '';
if (isCwdInstalled) {
// 读取已安装包的package.json
try {
const installedPackageJson = (0, json_1.jsonParse)(fs_1.default.readFileSync(path_1.default.join(depCwdTargetDir, 'package.json'), 'utf-8'));
// 确保版本是字符串类型
installedVersion = String(installedPackageJson.version || '');
// 检查版本是否匹配
isVersionMatch = mergedOptions.force ? true : satisfiesVersion(installedVersion, String(version));
}
catch (error) {
// 可能是由于 npm 正在安装,导致文件还未解压成功,此时应该降级安装到当前依赖的 node_modules
logger_1.default.error(`读取已安装包的 package.json 失败: ${path_1.default.join(depCwdTargetDir, 'package.json')} 降级安装到当前依赖的 node_modules`);
}
}
if (isCwdInstalled && isVersionMatch) {
// 当force=true时,记录特殊日志提示可能的版本不兼容问题
if (mergedOptions.force && !satisfiesVersion(installedVersion, String(version))) {
logger_1.default.warn(`依赖 ${name}@${version} 采用 force 模式,使用已安装版本 ${installedVersion},可能存在版本不兼容风险`);
}
else {
logger_1.default.debug(`依赖 ${name}@${version} 已在当前工作目录安装并匹配,跳过安装`);
}
// 创建软链接到包的node_modules目录
if (!fs_1.default.existsSync(depPackageTargetDir)) {
// 确保父目录存在
fs_1.default.mkdirSync(path_1.default.dirname(depPackageTargetDir), { recursive: true });
// 创建软链接
try {
fs_1.default.symlinkSync(depCwdTargetDir, depPackageTargetDir, 'junction');
logger_1.default.debug(`为 ${name} 创建了软链接`);
}
catch (linkErr) {
logger_1.default.debug(`创建软链接失败,将复制目录: ${linkErr.message}`);
// 如果创建软链接失败,则复制目录
fs_1.default.cpSync(depCwdTargetDir, depPackageTargetDir, { recursive: true });
}
}
}
else {
if (isCwdInstalled) {
logger_1.default.debug(`依赖 ${name} == ${installedVersion} 不匹配需求版本 ${version},将安装新版本`);
}
// 不匹配或未安装,则在工作目录中安装依赖
await installPackage(depPackageName, depCwdTargetDir, mergedOptions);
// 创建软链接到包的node_modules目录
if (!fs_1.default.existsSync(depPackageTargetDir)) {
try {
fs_1.default.symlinkSync(depCwdTargetDir, depPackageTargetDir, 'junction');
logger_1.default.debug(`为 ${name} 创建了软链接`);
}
catch (linkErr) {
logger_1.default.debug(`创建软链接失败,将复制目录: ${linkErr.message}`);
fs_1.default.cpSync(depCwdTargetDir, depPackageTargetDir, { recursive: true });
}
}
// 递归安装依赖的依赖
await installDependencies(depCwdTargetDir, mergedOptions);
}
completed++;
logger_1.default.updateProgress(completed);
logger_1.default.debug(`依赖处理完成: ${depPackageName}`);
}
catch (error) {
completed++;
logger_1.default.updateProgress(completed);
throw error;
}
}
};
// 启动并发任务
const concurrencyLevel = Math.min(mergedOptions.concurrency, dependencyCount);
const tasks = Array(concurrencyLevel).fill(0).map(() => installConcurrently());
await Promise.all(tasks);
logger_1.default.completeProgress();
logger_1.default.success(`所有依赖安装完成: ${path_1.default.basename(packageDir)}`);
}
catch (error) {
const enhancedError = logger_1.default.wrapError(error, `安装 ${path_1.default.basename(packageDir)} 的依赖失败`);
logger_1.default.error(enhancedError);
throw enhancedError;
}
}
/**
* 简单检查版本是否满足要求
* @param installed 已安装版本
* @param required 需求版本
* @returns 是否满足要求
*/
function satisfiesVersion(installed, required) {
// 移除版本范围符号,取最小版本进行比较
// 这里是简化版,完整版应该使用semver库进行比较
const cleanRequired = required.replace(/[\^~>=<]/g, '');
if (required.startsWith('^')) {
// ^1.2.3 表示兼容 1.x.x
const [major1] = installed.split('.');
const [major2] = cleanRequired.split('.');
return major1 === major2;
}
else if (required.startsWith('~')) {
// ~1.2.3 表示兼容 1.2.x
const [major1, minor1] = installed.split('.');
const [major2, minor2] = cleanRequired.split('.');
return major1 === major2 && minor1 === minor2;
}
else if (required.includes('||')) {
// 1.0.0 || 2.0.0 表示匹配其中任一版本
const versions = required.split('||').map(v => v.trim());
return versions.some(v => satisfiesVersion(installed, v));
}
else if (required.startsWith('>=')) {
// >=1.2.3 表示大于等于该版本
return compareVersions(installed, cleanRequired) >= 0;
}
else if (required.startsWith('>')) {
// >1.2.3 表示大于该版本
return compareVersions(installed, cleanRequired) > 0;
}
else if (required.startsWith('<=')) {
// <=1.2.3 表示小于等于该版本
return compareVersions(installed, cleanRequired) <= 0;
}
else if (required.startsWith('<')) {
// <1.2.3 表示小于该版本
return compareVersions(installed, cleanRequired) < 0;
}
else {
// 精确版本匹配
return installed === cleanRequired;
}
}
/**
* 比较两个版本号
* @param v1 版本1
* @param v2 版本2
* @returns 比较结果
*/
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const part1 = i < parts1.length ? parts1[i] : 0;
const part2 = i < parts2.length ? parts2[i] : 0;
if (part1 > part2)
return 1;
if (part1 < part2)
return -1;
}
return 0;
}
/**
* 完整安装流程
* @param packageName 包名称
* @param targetDir 目标目录(默认为当前目录下的node_modules)
* @param options 选项
*/
async function install(packageName, targetDir, options = {}) {
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
// 设置日志级别
if (mergedOptions.verbose) {
logger_1.default.setLevel(0); // DEBUG
}
else if (mergedOptions.silent) {
logger_1.default.setLevel(4); // SILENT
}
try {
logger_1.default.info(`开始安装包: ${packageName}`);
// 解析包名和版本,处理scoped packages
const currentDir = process.cwd();
const nodeModulesDir = path_1.default.join(currentDir, 'node_modules');
// 解析包名:处理 @scope/name@version 或 name@version 格式
let packageBaseName;
if (packageName.startsWith('@')) {
// 处理 scoped package
const scopedParts = packageName.split('@');
// 对于 @scope/name 或 @scope/name@version
if (scopedParts.length >= 2) {
const scopePart = scopedParts[1];
const nameParts = scopePart.split('/');
if (nameParts.length >= 2) {
// 处理 @scope/name@version
if (scopedParts.length >= 3) {
packageBaseName = `@${nameParts[0]}/${nameParts[1].split('@')[0]}`;
}
else {
// 处理 @scope/name
packageBaseName = `@${nameParts[0]}/${nameParts[1]}`;
}
}
else {
// 处理简单的 @scope
packageBaseName = `@${scopePart}`;
}
}
}
else {
// 处理 name 或 name@version
packageBaseName = packageName.split('@')[0];
}
// 确保 packageBaseName 不为空
if (!packageBaseName) {
throw new Error(`无法解析包名: ${packageName}`);
}
// 创建安装目标目录
const defaultTargetDir = path_1.default.join(nodeModulesDir, packageBaseName);
const finalTargetDir = targetDir || defaultTargetDir;
// 确保 node_modules 目录存在
fs_1.default.mkdirSync(nodeModulesDir, { recursive: true });
logger_1.default.debug(`安装目标目录: ${finalTargetDir}`);
logger_1.default.debug(`使用registry: ${mergedOptions.registry}`);
// 安装主包
const startTime = Date.now();
await installPackage(packageName, finalTargetDir, mergedOptions);
// 递归安装依赖
await installDependencies(finalTargetDir, mergedOptions);
const totalTime = ((Date.now() - startTime) / 1000).toFixed(2);
logger_1.default.success(`包 ${packageName} 安装完成,耗时 ${totalTime} 秒`);
logger_1.default.info(`安装路径: ${finalTargetDir}`);
}
catch (error) {
const enhancedError = logger_1.default.wrapError(error, `安装 ${packageName} 失败`);
logger_1.default.error(enhancedError);
throw enhancedError;
}
}
exports.default = {
install,
installPackage,
installDependencies
};