#!/usr/bin/env zx import 'zx/globals'; const ROOT_DIR = path.resolve(__dirname, '..'); const NODE_VERSION = '22.16.0'; const BASE_URL = `https://nodejs.org/dist/v${NODE_VERSION}`; const OUTPUT_BASE = path.join(ROOT_DIR, 'resources', 'bin'); const TARGETS = { 'darwin-x64': { filename: `node-v${NODE_VERSION}-darwin-x64.tar.gz`, sourceDir: `node-v${NODE_VERSION}-darwin-x64`, }, 'darwin-arm64': { filename: `node-v${NODE_VERSION}-darwin-arm64.tar.gz`, sourceDir: `node-v${NODE_VERSION}-darwin-arm64`, }, 'win32-x64': { filename: `node-v${NODE_VERSION}-win-x64.zip`, sourceDir: `node-v${NODE_VERSION}-win-x64`, }, 'win32-arm64': { filename: `node-v${NODE_VERSION}-win-arm64.zip`, sourceDir: `node-v${NODE_VERSION}-win-arm64`, }, }; const PLATFORM_GROUPS = { mac: ['darwin-x64', 'darwin-arm64'], win: ['win32-x64', 'win32-arm64'], }; async function setupTarget(id) { const target = TARGETS[id]; if (!target) { echo(chalk.yellow`āš ļø Target ${id} is not supported by this script.`); return; } const targetDir = path.join(OUTPUT_BASE, id); const tempDir = path.join(ROOT_DIR, 'temp_node_extract'); const archivePath = path.join(ROOT_DIR, target.filename); const downloadUrl = `${BASE_URL}/${target.filename}`; echo(chalk.blue`\nšŸ“¦ Setting up Node.js for ${id}...`); const outputNode = path.join(targetDir, id.startsWith('win32-') ? 'node.exe' : 'node'); const outputNpmDir = path.join(targetDir, 'lib', 'node_modules', 'npm'); const outputNpmCli = path.join(outputNpmDir, 'bin', 'npm-cli.js'); if (process.env.FORCE_BUNDLED_NODE_REFRESH !== '1' && await fs.pathExists(outputNode) && await fs.pathExists(outputNpmCli)) { echo(chalk.green`āœ… Already prepared: ${outputNode}`); return; } await fs.remove(tempDir); await fs.ensureDir(targetDir); await fs.ensureDir(tempDir); try { echo`ā¬‡ļø Downloading: ${downloadUrl}`; const response = await fetchWithRetry(downloadUrl); const buffer = await response.arrayBuffer(); await fs.writeFile(archivePath, Buffer.from(buffer)); echo`šŸ“‚ Extracting...`; if (target.filename.endsWith('.zip')) { const { execFileSync } = await import('child_process'); if (os.platform() === 'win32') { const psCommand = `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${archivePath.replace(/'/g, "''")}', '${tempDir.replace(/'/g, "''")}')`; execFileSync('powershell.exe', ['-NoProfile', '-Command', psCommand], { stdio: 'inherit' }); } else { await $`unzip -q -o ${archivePath} -d ${tempDir}`; } } else { await $`tar -xzf ${archivePath} -C ${tempDir}`; } const nodeFileName = id.startsWith('win32-') ? 'node.exe' : 'node'; const expectedNode = path.join(tempDir, target.sourceDir, 'bin', nodeFileName); const legacyExpectedNode = path.join(tempDir, target.sourceDir, nodeFileName); if (await fs.pathExists(expectedNode)) { await fs.move(expectedNode, outputNode, { overwrite: true }); } else if (await fs.pathExists(legacyExpectedNode)) { await fs.move(legacyExpectedNode, outputNode, { overwrite: true }); } else { echo(chalk.yellow`šŸ” ${nodeFileName} not found in expected directory, searching...`); const files = await glob(`**/${nodeFileName}`, { cwd: tempDir, absolute: true }); if (files.length > 0) { await fs.move(files[0], outputNode, { overwrite: true }); } else { throw new Error(`Could not find ${nodeFileName} in extracted files.`); } } if (!id.startsWith('win32-')) { await fs.chmod(outputNode, 0o755); } const sourceNpmDir = path.join(tempDir, target.sourceDir, 'lib', 'node_modules', 'npm'); if (await fs.pathExists(sourceNpmDir)) { await fs.ensureDir(path.dirname(outputNpmDir)); await fs.copy(sourceNpmDir, outputNpmDir, { overwrite: true }); } else { echo(chalk.yellow`āš ļø npm package not found in Node archive for ${id}; only node binary was copied.`); } echo(chalk.green`āœ… Success: ${outputNode}`); } finally { await fs.remove(archivePath); await fs.remove(tempDir); } } async function fetchWithRetry(url, attempts = 3) { let lastError; for (let attempt = 1; attempt <= attempts; attempt += 1) { try { const response = await fetch(url); if (!response.ok) throw new Error(`Failed to download: ${response.status} ${response.statusText}`); return response; } catch (error) { lastError = error; if (attempt < attempts) { echo(chalk.yellow`āš ļø Download failed (attempt ${attempt}/${attempts}), retrying...`); await new Promise((resolve) => setTimeout(resolve, 1500 * attempt)); } } } throw lastError; } const downloadAll = argv.all; const platform = argv.platform; const target = argv.target; if (target) { await setupTarget(String(target)); } else if (downloadAll) { echo(chalk.cyan`🌐 Downloading Node.js binaries for all bundled targets...`); for (const id of Object.keys(TARGETS)) { await setupTarget(id); } } else if (platform) { const targets = PLATFORM_GROUPS[platform]; if (!targets) { echo(chalk.red`āŒ Unknown platform: ${platform}`); echo(`Available platforms: ${Object.keys(PLATFORM_GROUPS).join(', ')}`); process.exit(1); } echo(chalk.cyan`šŸŽÆ Downloading Node.js binaries for platform: ${platform}`); for (const id of targets) { await setupTarget(id); } } else { const currentId = `${os.platform()}-${os.arch()}`; if (TARGETS[currentId]) { echo(chalk.cyan`šŸ’» Detected system: ${currentId}`); await setupTarget(currentId); } else { echo(chalk.cyan`šŸŽÆ Defaulting to Windows multi-arch Node.js download`); for (const id of PLATFORM_GROUPS.win) { await setupTarget(id); } } } echo(chalk.green`\nšŸŽ‰ Done!`);