nodejs-dependencies-install.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. #!/usr/bin/env node
  2. /**
  3. * Node.js 依赖:安装(package.json → nodejs/node/node_modules)与同步(→ dependencies.txt)
  4. * 用法:node nodejs-dependencies-install.js [--update]
  5. * 无参数:检查 package.json,安装缺失依赖,并同步到 dependencies.txt
  6. * --update:仅对比 node_modules 与 dependencies.txt,不一致时更新 dependencies.txt
  7. */
  8. const fs = require('fs');
  9. const path = require('path');
  10. const { execSync } = require('child_process');
  11. const scriptDir = __dirname;
  12. const nodejsDir = path.dirname(path.dirname(scriptDir));
  13. const projectRoot = path.dirname(nodejsDir);
  14. const nodeDir = path.join(nodejsDir, 'node');
  15. const nodeModulesPath = path.join(nodeDir, 'node_modules');
  16. const packageJsonPath = path.join(projectRoot, 'package.json');
  17. const dependenciesFile = path.join(scriptDir, 'dependencies.txt');
  18. const nodeExe = path.join(nodeDir, process.platform === 'win32' ? 'node.exe' : 'node');
  19. const npmCli = path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js');
  20. if (!fs.existsSync(nodeDir) || !fs.existsSync(npmCli)) {
  21. console.error('[X] nodejs/node or nodejs/node/node_modules/npm not found. Run nodejs/install-node-modules.bat first.');
  22. process.exit(1);
  23. }
  24. process.env.PATH = nodeDir + path.delimiter + (process.env.PATH || '');
  25. const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', white: '\x1b[37m' };
  26. function log(message, color = 'reset') {
  27. console.log(`${colors[color]}${message}${colors.reset}`);
  28. }
  29. /** 从 node_modules 读取已安装包,返回 { [nameLower]: 'name==version' },排除 npm、corepack */
  30. function getInstalledPackages() {
  31. const out = {};
  32. if (!fs.existsSync(nodeModulesPath)) return out;
  33. try {
  34. const entries = fs.readdirSync(nodeModulesPath, { withFileTypes: true });
  35. for (const entry of entries) {
  36. if (!entry.isDirectory()) continue;
  37. const packageName = entry.name;
  38. if (packageName.startsWith('.') || packageName === 'node_modules' || packageName === 'npm' || packageName === 'corepack') continue;
  39. const pkgPath = path.join(nodeModulesPath, packageName, 'package.json');
  40. if (fs.existsSync(pkgPath)) {
  41. try {
  42. const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
  43. if (pkg.name && pkg.version) out[pkg.name.toLowerCase()] = `${pkg.name}==${pkg.version}`;
  44. } catch (_) {}
  45. }
  46. if (packageName.startsWith('@')) {
  47. try {
  48. const scopedPath = path.join(nodeModulesPath, packageName);
  49. const sub = fs.readdirSync(scopedPath, { withFileTypes: true });
  50. for (const e of sub) {
  51. if (!e.isDirectory()) continue;
  52. const subPath = path.join(scopedPath, e.name, 'package.json');
  53. if (fs.existsSync(subPath)) {
  54. try {
  55. const pkg = JSON.parse(fs.readFileSync(subPath, 'utf-8'));
  56. if (pkg.name && pkg.version) out[pkg.name.toLowerCase()] = `${pkg.name}==${pkg.version}`;
  57. } catch (_) {}
  58. }
  59. }
  60. } catch (_) {}
  61. }
  62. }
  63. } catch (_) {}
  64. return out;
  65. }
  66. /** 读取 dependencies.txt 为 { [nameLower]: line } */
  67. function readDependenciesFile() {
  68. const out = {};
  69. if (!fs.existsSync(dependenciesFile)) return out;
  70. const content = fs.readFileSync(dependenciesFile, 'utf-8');
  71. for (const line of content.split('\n')) {
  72. const t = line.trim();
  73. if (!t || t.startsWith('#')) continue;
  74. const name = t.includes('==') ? t.split('==', 2)[0].trim() : t.split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('~=')[0].trim();
  75. if (name) out[name.toLowerCase()] = t;
  76. }
  77. return out;
  78. }
  79. /** 将已安装列表写入 dependencies.txt */
  80. function writeDependenciesFile(installed) {
  81. const lines = [...new Set(Object.values(installed))].sort();
  82. fs.writeFileSync(dependenciesFile, lines.join('\n') + '\n', 'utf-8');
  83. }
  84. /** 仅更新模式:对比 node_modules 与 dependencies.txt,不一致则写回 */
  85. function runUpdateOnly() {
  86. log('Comparing node_modules with dependencies.txt...', 'cyan');
  87. const installed = getInstalledPackages();
  88. if (Object.keys(installed).length === 0) {
  89. log('[ERROR] nodejs/node/node_modules not found or empty.', 'red');
  90. process.exit(1);
  91. }
  92. const filePkgs = readDependenciesFile();
  93. const instSet = new Set(Object.keys(installed));
  94. const fileSet = new Set(Object.keys(filePkgs));
  95. const added = [...instSet].filter(k => !fileSet.has(k));
  96. const removed = [...fileSet].filter(k => !instSet.has(k));
  97. const changed = [...instSet].filter(k => fileSet.has(k) && installed[k] !== filePkgs[k]);
  98. if (added.length) {
  99. log(`\n[+] Added (${added.length}):`, 'green');
  100. added.sort().slice(0, 10).forEach(k => log(` + ${installed[k]}`, 'green'));
  101. if (added.length > 10) log(` ... and ${added.length - 10} more`, 'green');
  102. }
  103. if (removed.length) {
  104. log(`\n[-] Removed (${removed.length}):`, 'red');
  105. removed.sort().slice(0, 10).forEach(k => log(` - ${filePkgs[k]}`, 'red'));
  106. if (removed.length > 10) log(` ... and ${removed.length - 10} more`, 'red');
  107. }
  108. if (changed.length) {
  109. log(`\n[~] Changed (${changed.length}):`, 'yellow');
  110. changed.sort().slice(0, 10).forEach(k => log(` ~ ${filePkgs[k]} -> ${installed[k]}`, 'yellow'));
  111. if (changed.length > 10) log(` ... and ${changed.length - 10} more`, 'yellow');
  112. }
  113. if (added.length === 0 && removed.length === 0 && changed.length === 0) {
  114. log(`\n[OK] dependencies.txt is up to date (${Object.keys(installed).length} packages)`, 'green');
  115. return;
  116. }
  117. log(`\nUpdating ${dependenciesFile}...`, 'cyan');
  118. writeDependenciesFile(installed);
  119. log(`[OK] dependencies.txt updated (${Object.keys(installed).length} packages)`, 'green');
  120. }
  121. function main() {
  122. const updateOnly = process.argv.includes('--update');
  123. if (updateOnly) {
  124. runUpdateOnly();
  125. process.exit(0);
  126. }
  127. if (!fs.existsSync(packageJsonPath)) {
  128. log('[X] package.json not found', 'red');
  129. process.exit(1);
  130. }
  131. const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
  132. const allDependencies = {};
  133. if (packageJson.dependencies) {
  134. Object.keys(packageJson.dependencies).forEach(depName => {
  135. allDependencies[depName] = { version: packageJson.dependencies[depName], type: 'dependency' };
  136. });
  137. }
  138. if (packageJson.devDependencies) {
  139. Object.keys(packageJson.devDependencies).forEach(depName => {
  140. allDependencies[depName] = { version: packageJson.devDependencies[depName], type: 'devDependency' };
  141. });
  142. }
  143. const installed = getInstalledPackages();
  144. const depNames = Object.keys(allDependencies).sort();
  145. const missingDependencies = depNames.filter(depName => !installed[depName.toLowerCase()]);
  146. let installedCount = depNames.length - missingDependencies.length;
  147. let missingCount = missingDependencies.length;
  148. if (missingCount > 0) {
  149. log(`[X] Missing ${missingCount} package(s) out of ${depNames.length}`, 'red');
  150. log('Missing dependencies:', 'yellow');
  151. missingDependencies.forEach(m => log(` - ${m}`, 'red'));
  152. } else {
  153. log(`[OK] All dependencies are installed (${depNames.length} packages)`, 'green');
  154. writeDependenciesFile(getInstalledPackages());
  155. process.exit(0);
  156. }
  157. let maxRetries = 5;
  158. let retryCount = 0;
  159. let currentMissing = [...missingDependencies];
  160. const npmInstallPrefix = `--prefix ${path.join(nodeDir).replace(/\\/g, '/')}`;
  161. const registries = [
  162. { name: 'Tencent Cloud', url: 'https://mirrors.cloud.tencent.com/npm/' },
  163. { name: 'Huawei Cloud', url: 'https://repo.huaweicloud.com/repository/npm/' },
  164. { name: 'Taobao Mirror', url: 'https://registry.npmmirror.com' }
  165. ];
  166. while (currentMissing.length > 0 && retryCount < maxRetries) {
  167. if (retryCount > 0) log(`\nRetry ${retryCount}/${maxRetries - 1}...`, 'cyan');
  168. log('\nInstalling missing dependencies...', 'yellow');
  169. process.chdir(projectRoot);
  170. let installSuccess = false;
  171. for (const registry of registries) {
  172. log(`\nTrying ${registry.name} registry...`, 'cyan');
  173. try {
  174. execSync(`npm config set registry ${registry.url}`, { stdio: 'inherit', cwd: projectRoot, env: process.env });
  175. execSync(`npm install ${npmInstallPrefix} --ignore-engines`, { stdio: 'inherit', cwd: projectRoot, encoding: 'utf-8', env: process.env });
  176. } catch (_) {}
  177. const after = getInstalledPackages();
  178. const stillMissing = depNames.filter(depName => !after[depName.toLowerCase()]);
  179. if (stillMissing.length === 0) {
  180. installSuccess = true;
  181. log(`\n[OK] Installation successful using ${registry.name}`, 'green');
  182. break;
  183. }
  184. log(`\n[~] ${stillMissing.length} still missing, trying next registry...`, 'yellow');
  185. }
  186. if (!installSuccess) {
  187. try {
  188. log('\nTrying default npm registry...', 'cyan');
  189. execSync('npm config set registry https://registry.npmjs.org/', { stdio: 'inherit', cwd: projectRoot, env: process.env });
  190. execSync(`npm install ${npmInstallPrefix} --ignore-engines`, { stdio: 'inherit', cwd: projectRoot, env: process.env });
  191. } catch (_) {}
  192. }
  193. const after = getInstalledPackages();
  194. const stillMissing = depNames.filter(depName => !after[depName.toLowerCase()]);
  195. if (stillMissing.length === 0) {
  196. log('[OK] All dependencies installed successfully', 'green');
  197. break;
  198. }
  199. if (stillMissing.length < currentMissing.length) {
  200. log(`[~] Progress: ${currentMissing.length - stillMissing.length} installed, ${stillMissing.length} remaining`, 'yellow');
  201. currentMissing = stillMissing;
  202. retryCount++;
  203. } else {
  204. log(`[X] Still missing ${stillMissing.length} package(s)`, 'red');
  205. stillMissing.slice(0, 10).forEach(m => log(` - ${m}`, 'red'));
  206. currentMissing = stillMissing;
  207. retryCount++;
  208. }
  209. }
  210. const finalInstalled = getInstalledPackages();
  211. const stillMissing = depNames.filter(depName => !finalInstalled[depName.toLowerCase()]);
  212. if (stillMissing.length > 0) {
  213. log(`\n[X] Failed to install ${stillMissing.length} package(s)`, 'red');
  214. stillMissing.forEach(m => log(` - ${m}`, 'red'));
  215. process.exit(1);
  216. }
  217. writeDependenciesFile(finalInstalled);
  218. process.exit(0);
  219. }
  220. main();