bootstrap-npm.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. #!/usr/bin/env node
  2. /**
  3. * 安装 npm 到 nodejs/node/node_modules(当 node 自带 npm 缺失时使用)
  4. * 优先从 nodejs/backup/npm 拷贝;备份不可用再从 registry 下载。
  5. */
  6. const fs = require('fs');
  7. const path = require('path');
  8. const https = require('https');
  9. const zlib = require('zlib');
  10. const NPM_REGISTRY = 'https://registry.npmmirror.com';
  11. const nodeDir = path.join(__dirname, 'node');
  12. const nodeModulesDir = path.join(nodeDir, 'node_modules');
  13. const npmDir = path.join(nodeModulesDir, 'npm');
  14. const npmBackupDir = path.join(__dirname, 'backup', 'npm');
  15. function isNpmBundleComplete(dir) {
  16. if (!fs.existsSync(path.join(dir, 'bin', 'npm-cli.js'))) return false;
  17. // npm's own lib/cli/entry.js requires graceful-fs from npm/node_modules
  18. return fs.existsSync(path.join(dir, 'node_modules', 'graceful-fs', 'package.json'));
  19. }
  20. function get(url) {
  21. return new Promise((resolve, reject) => {
  22. const req = https.get(url, { headers: { 'User-Agent': 'Node/bootstrap-npm' } }, (res) => {
  23. if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
  24. const loc = res.headers.location;
  25. return get(loc.startsWith('http') ? loc : new URL(loc, url).href).then(resolve).catch(reject);
  26. }
  27. const chunks = [];
  28. res.on('data', (c) => chunks.push(c));
  29. res.on('end', () => resolve(Buffer.concat(chunks)));
  30. res.on('error', reject);
  31. });
  32. req.on('error', reject);
  33. });
  34. }
  35. function extractTar(buffer, outDir) {
  36. let offset = 0;
  37. while (offset + 512 <= buffer.length) {
  38. const header = buffer.slice(offset, offset + 512);
  39. if (header.every(b => b === 0)) break;
  40. const name = header.slice(0, 100).toString('utf8').replace(/\0/g, '');
  41. const size = parseInt(header.slice(124, 136).toString('utf8').trim(), 8) || 0;
  42. const typeflag = (header[156] && header[156] !== 0) ? String.fromCharCode(header[156]) : '0';
  43. offset += 512;
  44. const content = buffer.slice(offset, offset + size);
  45. offset += Math.ceil(size / 512) * 512;
  46. if (!name || name.includes('..')) continue;
  47. const dest = path.join(outDir, name);
  48. if (typeflag === '5' || name.endsWith('/')) {
  49. if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
  50. } else if (size >= 0) {
  51. const dir = path.dirname(dest);
  52. if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  53. fs.writeFileSync(dest, content);
  54. }
  55. }
  56. }
  57. async function main() {
  58. if (!fs.existsSync(path.join(nodeDir, 'node.exe'))) {
  59. console.error('[X] nodejs/node/node.exe not found');
  60. process.exit(1);
  61. }
  62. if (isNpmBundleComplete(npmDir)) {
  63. console.log('[OK] npm already present in node_modules');
  64. return;
  65. }
  66. if (fs.existsSync(npmDir)) {
  67. console.log('[WARN] npm folder missing or incomplete; will replace...');
  68. try {
  69. fs.rmSync(npmDir, { recursive: true, force: true });
  70. } catch (e) {
  71. console.error('[X] Could not remove broken npm folder:', npmDir, e.message);
  72. process.exit(1);
  73. }
  74. }
  75. if (isNpmBundleComplete(npmBackupDir)) {
  76. if (!fs.existsSync(nodeModulesDir)) fs.mkdirSync(nodeModulesDir, { recursive: true });
  77. fs.cpSync(npmBackupDir, npmDir, { recursive: true });
  78. console.log('[OK] npm restored from nodejs/backup/npm (no registry download)');
  79. return;
  80. }
  81. console.log('[WARN] nodejs/backup/npm missing or incomplete; fetching npm from registry...');
  82. console.log('Fetching npm metadata from registry...');
  83. const meta = JSON.parse((await get(`${NPM_REGISTRY}/npm/latest`)).toString());
  84. const version = meta.version;
  85. const tarball = meta.dist?.tarball || `${NPM_REGISTRY}/npm/-/npm-${version}.tgz`;
  86. console.log('Downloading npm@' + version + '...');
  87. const tgz = await get(tarball);
  88. if (tgz.length < 1000 && tgz.toString().includes('<!')) {
  89. throw new Error('Registry returned HTML instead of tarball.');
  90. }
  91. if (tgz[0] !== 0x1f || tgz[1] !== 0x8b) {
  92. throw new Error('Downloaded file is not gzip.');
  93. }
  94. if (!fs.existsSync(nodeModulesDir)) fs.mkdirSync(nodeModulesDir, { recursive: true });
  95. const extractDir = path.join(nodeDir, 'npm-extract');
  96. if (fs.existsSync(extractDir)) { try { fs.rmSync(extractDir, { recursive: true }); } catch (_) {} }
  97. fs.mkdirSync(extractDir, { recursive: true });
  98. const tar = zlib.gunzipSync(tgz);
  99. extractTar(tar, extractDir);
  100. const pkgDir = path.join(extractDir, 'package');
  101. if (fs.existsSync(npmDir)) fs.rmSync(npmDir, { recursive: true });
  102. fs.renameSync(fs.existsSync(pkgDir) ? pkgDir : path.join(extractDir, fs.readdirSync(extractDir)[0]), npmDir);
  103. try { fs.rmSync(extractDir, { recursive: true }); } catch (_) {}
  104. console.log('[OK] npm installed to node_modules');
  105. }
  106. main().catch((err) => { console.error(err); process.exit(1); });