#!/usr/bin/env node /** * Node.js 依赖:安装(package.json → nodejs/node/node_modules)与同步(→ dependencies.txt) * 用法:node nodejs-dependencies-install.js [--update] * 无参数:检查 package.json,安装缺失依赖,并同步到 dependencies.txt * --update:仅对比 node_modules 与 dependencies.txt,不一致时更新 dependencies.txt */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const scriptDir = __dirname; const nodejsDir = path.dirname(path.dirname(scriptDir)); const projectRoot = path.dirname(nodejsDir); const nodeDir = path.join(nodejsDir, 'node'); const nodeModulesPath = path.join(nodeDir, 'node_modules'); const packageJsonPath = path.join(projectRoot, 'package.json'); const dependenciesFile = path.join(scriptDir, 'dependencies.txt'); const nodeExe = path.join(nodeDir, process.platform === 'win32' ? 'node.exe' : 'node'); const npmCli = path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'); if (!fs.existsSync(nodeDir) || !fs.existsSync(npmCli)) { console.error('[X] nodejs/node or nodejs/node/node_modules/npm not found. Run nodejs/install-node-modules.bat first.'); process.exit(1); } process.env.PATH = nodeDir + path.delimiter + (process.env.PATH || ''); const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', white: '\x1b[37m' }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } /** 从 node_modules 读取已安装包,返回 { [nameLower]: 'name==version' },排除 npm、corepack */ function getInstalledPackages() { const out = {}; if (!fs.existsSync(nodeModulesPath)) return out; try { const entries = fs.readdirSync(nodeModulesPath, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const packageName = entry.name; if (packageName.startsWith('.') || packageName === 'node_modules' || packageName === 'npm' || packageName === 'corepack') continue; const pkgPath = path.join(nodeModulesPath, packageName, 'package.json'); if (fs.existsSync(pkgPath)) { try { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); if (pkg.name && pkg.version) out[pkg.name.toLowerCase()] = `${pkg.name}==${pkg.version}`; } catch (_) {} } if (packageName.startsWith('@')) { try { const scopedPath = path.join(nodeModulesPath, packageName); const sub = fs.readdirSync(scopedPath, { withFileTypes: true }); for (const e of sub) { if (!e.isDirectory()) continue; const subPath = path.join(scopedPath, e.name, 'package.json'); if (fs.existsSync(subPath)) { try { const pkg = JSON.parse(fs.readFileSync(subPath, 'utf-8')); if (pkg.name && pkg.version) out[pkg.name.toLowerCase()] = `${pkg.name}==${pkg.version}`; } catch (_) {} } } } catch (_) {} } } } catch (_) {} return out; } /** 读取 dependencies.txt 为 { [nameLower]: line } */ function readDependenciesFile() { const out = {}; if (!fs.existsSync(dependenciesFile)) return out; const content = fs.readFileSync(dependenciesFile, 'utf-8'); for (const line of content.split('\n')) { const t = line.trim(); if (!t || t.startsWith('#')) continue; const name = t.includes('==') ? t.split('==', 2)[0].trim() : t.split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('~=')[0].trim(); if (name) out[name.toLowerCase()] = t; } return out; } /** 将已安装列表写入 dependencies.txt */ function writeDependenciesFile(installed) { const lines = [...new Set(Object.values(installed))].sort(); fs.writeFileSync(dependenciesFile, lines.join('\n') + '\n', 'utf-8'); } /** 仅更新模式:对比 node_modules 与 dependencies.txt,不一致则写回 */ function runUpdateOnly() { log('Comparing node_modules with dependencies.txt...', 'cyan'); const installed = getInstalledPackages(); if (Object.keys(installed).length === 0) { log('[ERROR] nodejs/node/node_modules not found or empty.', 'red'); process.exit(1); } const filePkgs = readDependenciesFile(); const instSet = new Set(Object.keys(installed)); const fileSet = new Set(Object.keys(filePkgs)); const added = [...instSet].filter(k => !fileSet.has(k)); const removed = [...fileSet].filter(k => !instSet.has(k)); const changed = [...instSet].filter(k => fileSet.has(k) && installed[k] !== filePkgs[k]); if (added.length) { log(`\n[+] Added (${added.length}):`, 'green'); added.sort().slice(0, 10).forEach(k => log(` + ${installed[k]}`, 'green')); if (added.length > 10) log(` ... and ${added.length - 10} more`, 'green'); } if (removed.length) { log(`\n[-] Removed (${removed.length}):`, 'red'); removed.sort().slice(0, 10).forEach(k => log(` - ${filePkgs[k]}`, 'red')); if (removed.length > 10) log(` ... and ${removed.length - 10} more`, 'red'); } if (changed.length) { log(`\n[~] Changed (${changed.length}):`, 'yellow'); changed.sort().slice(0, 10).forEach(k => log(` ~ ${filePkgs[k]} -> ${installed[k]}`, 'yellow')); if (changed.length > 10) log(` ... and ${changed.length - 10} more`, 'yellow'); } if (added.length === 0 && removed.length === 0 && changed.length === 0) { log(`\n[OK] dependencies.txt is up to date (${Object.keys(installed).length} packages)`, 'green'); return; } log(`\nUpdating ${dependenciesFile}...`, 'cyan'); writeDependenciesFile(installed); log(`[OK] dependencies.txt updated (${Object.keys(installed).length} packages)`, 'green'); } function main() { const updateOnly = process.argv.includes('--update'); if (updateOnly) { runUpdateOnly(); process.exit(0); } if (!fs.existsSync(packageJsonPath)) { log('[X] package.json not found', 'red'); process.exit(1); } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); const allDependencies = {}; if (packageJson.dependencies) { Object.keys(packageJson.dependencies).forEach(depName => { allDependencies[depName] = { version: packageJson.dependencies[depName], type: 'dependency' }; }); } if (packageJson.devDependencies) { Object.keys(packageJson.devDependencies).forEach(depName => { allDependencies[depName] = { version: packageJson.devDependencies[depName], type: 'devDependency' }; }); } const installed = getInstalledPackages(); const depNames = Object.keys(allDependencies).sort(); const missingDependencies = depNames.filter(depName => !installed[depName.toLowerCase()]); let installedCount = depNames.length - missingDependencies.length; let missingCount = missingDependencies.length; if (missingCount > 0) { log(`[X] Missing ${missingCount} package(s) out of ${depNames.length}`, 'red'); log('Missing dependencies:', 'yellow'); missingDependencies.forEach(m => log(` - ${m}`, 'red')); } else { log(`[OK] All dependencies are installed (${depNames.length} packages)`, 'green'); writeDependenciesFile(getInstalledPackages()); process.exit(0); } let maxRetries = 5; let retryCount = 0; let currentMissing = [...missingDependencies]; const npmInstallPrefix = `--prefix ${path.join(nodeDir).replace(/\\/g, '/')}`; const registries = [ { name: 'Tencent Cloud', url: 'https://mirrors.cloud.tencent.com/npm/' }, { name: 'Huawei Cloud', url: 'https://repo.huaweicloud.com/repository/npm/' }, { name: 'Taobao Mirror', url: 'https://registry.npmmirror.com' } ]; while (currentMissing.length > 0 && retryCount < maxRetries) { if (retryCount > 0) log(`\nRetry ${retryCount}/${maxRetries - 1}...`, 'cyan'); log('\nInstalling missing dependencies...', 'yellow'); process.chdir(projectRoot); let installSuccess = false; for (const registry of registries) { log(`\nTrying ${registry.name} registry...`, 'cyan'); try { execSync(`npm config set registry ${registry.url}`, { stdio: 'inherit', cwd: projectRoot, env: process.env }); execSync(`npm install ${npmInstallPrefix} --ignore-engines`, { stdio: 'inherit', cwd: projectRoot, encoding: 'utf-8', env: process.env }); } catch (_) {} const after = getInstalledPackages(); const stillMissing = depNames.filter(depName => !after[depName.toLowerCase()]); if (stillMissing.length === 0) { installSuccess = true; log(`\n[OK] Installation successful using ${registry.name}`, 'green'); break; } log(`\n[~] ${stillMissing.length} still missing, trying next registry...`, 'yellow'); } if (!installSuccess) { try { log('\nTrying default npm registry...', 'cyan'); execSync('npm config set registry https://registry.npmjs.org/', { stdio: 'inherit', cwd: projectRoot, env: process.env }); execSync(`npm install ${npmInstallPrefix} --ignore-engines`, { stdio: 'inherit', cwd: projectRoot, env: process.env }); } catch (_) {} } const after = getInstalledPackages(); const stillMissing = depNames.filter(depName => !after[depName.toLowerCase()]); if (stillMissing.length === 0) { log('[OK] All dependencies installed successfully', 'green'); break; } if (stillMissing.length < currentMissing.length) { log(`[~] Progress: ${currentMissing.length - stillMissing.length} installed, ${stillMissing.length} remaining`, 'yellow'); currentMissing = stillMissing; retryCount++; } else { log(`[X] Still missing ${stillMissing.length} package(s)`, 'red'); stillMissing.slice(0, 10).forEach(m => log(` - ${m}`, 'red')); currentMissing = stillMissing; retryCount++; } } const finalInstalled = getInstalledPackages(); const stillMissing = depNames.filter(depName => !finalInstalled[depName.toLowerCase()]); if (stillMissing.length > 0) { log(`\n[X] Failed to install ${stillMissing.length} package(s)`, 'red'); stillMissing.forEach(m => log(` - ${m}`, 'red')); process.exit(1); } writeDependenciesFile(finalInstalled); process.exit(0); } main();