UNPKG

pacote-install

Version:

A tool to download npm packages from specific registries

392 lines (391 loc) 19.2 kB
"use strict"; 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 };