#!/usr/bin/env node /** * Postinstall script — runs automatically after `bun install` or `npm install`. * * Downloads ripgrep binary (idempotent, skips if exists). * Works in dev mode (src/ exists), published mode (dist/ exists), with bun or node. * * Usage: * node scripts/postinstall.js * node scripts/postinstall.js --force * bun run scripts/postinstall.js */ const { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, chmodSync } = require("fs") const { spawnSync } = require("child_process") const { setDefaultResultOrder } = require("node:dns") const path = require("path") const os = require("os") // Prefer IPv4 first — Bun on Windows sometimes fails GitHub over broken IPv6 paths. try { setDefaultResultOrder("ipv4first") } catch { /* ignore */ } // --- Config --- const RG_VERSION = "15.0.1" const DEFAULT_RELEASE_BASE = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}` const MIRROR_RELEASE_BASE = `https://ghproxy.net/https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}` const RELEASE_BASE = (process.env.RIPGREP_DOWNLOAD_BASE ?? DEFAULT_RELEASE_BASE).replace(/\/$/, "") const scriptDir = path.dirname(__filename) const projectRoot = path.resolve(scriptDir, "..") // --- Platform mapping --- function getPlatformMapping() { const arch = process.arch const platform = process.platform if (platform === "darwin") { if (arch === "arm64") return { target: "aarch64-apple-darwin", ext: "tar.gz" } if (arch === "x64") return { target: "x86_64-apple-darwin", ext: "tar.gz" } throw new Error(`Unsupported macOS arch: ${arch}`) } if (platform === "win32") { if (arch === "x64") return { target: "x86_64-pc-windows-msvc", ext: "zip" } if (arch === "arm64") return { target: "aarch64-pc-windows-msvc", ext: "zip" } throw new Error(`Unsupported Windows arch: ${arch}`) } if (platform === "linux") { const isMusl = detectMusl() if (arch === "x64") { return { target: "x86_64-unknown-linux-musl", ext: "tar.gz" } } if (arch === "arm64") { return isMusl ? { target: "aarch64-unknown-linux-musl", ext: "tar.gz" } : { target: "aarch64-unknown-linux-gnu", ext: "tar.gz" } } throw new Error(`Unsupported Linux arch: ${arch}`) } throw new Error(`Unsupported platform: ${platform}`) } function detectMusl() { const muslArch = process.arch === "x64" ? "x86_64" : "aarch64" try { statSync(`/lib/libc.musl-${muslArch}.so.1`) return true } catch { return false } } // --- Paths --- function getVendorDir() { if (existsSync(path.join(projectRoot, "src"))) { return path.resolve(projectRoot, "src", "utils", "vendor", "ripgrep") } return path.resolve(projectRoot, "dist", "vendor", "ripgrep") } function getBinaryPath() { const dir = getVendorDir() const subdir = `${process.arch}-${process.platform}` const binary = process.platform === "win32" ? "rg.exe" : "rg" return path.resolve(dir, subdir, binary) } // --- Download helpers --- function proxyEnvSet() { const v = (s) => (s ?? "").trim() return !!(v(process.env.HTTPS_PROXY) || v(process.env.HTTP_PROXY) || v(process.env.ALL_PROXY) || v(process.env.https_proxy) || v(process.env.http_proxy)) } function tryPowerShellDownload(url, dest) { const u = url.replace(/'/g, "''") const d = dest.replace(/'/g, "''") const cmd = `Invoke-WebRequest -Uri '${u}' -OutFile '${d}' -UseBasicParsing` const result = spawnSync( "powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", cmd], { stdio: "pipe", windowsHide: true }, ) return result.status === 0 && existsSync(dest) && statSync(dest).size > 0 } function tryCurlDownload(url, dest) { const curl = process.platform === "win32" ? "curl.exe" : "curl" const result = spawnSync(curl, ["-fsSL", "-L", "--fail", "-o", dest, url], { stdio: "pipe", windowsHide: true, }) return result.status === 0 && existsSync(dest) && statSync(dest).size > 0 } async function fetchRelease(url) { if (proxyEnvSet()) { // Dynamic require so it works in node without bundling issues const undici = require("undici") return await undici.fetch(url, { redirect: "follow", dispatcher: new undici.EnvHttpProxyAgent(), }) } // Node 18+ has global fetch, Bun has it too return await fetch(url, { redirect: "follow" }) } async function downloadUrlToBuffer(url) { const response = await fetchRelease(url) if (!response.ok) { throw new Error(`Download failed: ${response.status} ${response.statusText}`) } return Buffer.from(await response.arrayBuffer()) } async function downloadUrlToBufferWithFallback(url) { let firstError try { return await downloadUrlToBuffer(url) } catch (e) { firstError = e } const tmpRoot = path.join(os.tmpdir(), `ripgrep-dl-${process.pid}-${Date.now()}`) const tmpFile = path.join(tmpRoot, "archive") mkdirSync(tmpRoot, { recursive: true }) try { if (process.platform === "win32" && tryPowerShellDownload(url, tmpFile)) { return readFileSync(tmpFile) } if (tryCurlDownload(url, tmpFile)) { return readFileSync(tmpFile) } } finally { rmSync(tmpRoot, { recursive: true, force: true }) } throw firstError } // --- Extract --- function findZipEntryKey(files, want) { return Object.keys(files).find((k) => { const norm = k.replace(/\\/g, "/") return norm === want || norm.endsWith(`/${want}`) }) } async function extractZip(buffer, binaryPath, extractedBinary) { const binaryDir = path.dirname(binaryPath) // Try fflate first (bundled dep) let fflateError try { const { unzipSync } = require("fflate") const unzipped = unzipSync(new Uint8Array(buffer)) const key = findZipEntryKey(unzipped, extractedBinary) if (!key) { throw new Error(`Binary ${extractedBinary} not found in zip`) } writeFileSync(binaryPath, Buffer.from(unzipped[key])) return } catch (e) { fflateError = e } // Fallback: PowerShell Expand-Archive or unzip CLI const tmpDir = path.join(binaryDir, ".tmp-download") rmSync(tmpDir, { recursive: true, force: true }) mkdirSync(tmpDir, { recursive: true }) try { const assetName = `archive.zip` const archivePath = path.join(tmpDir, assetName) writeFileSync(archivePath, buffer) let extracted = false if (process.platform === "win32") { const psCmd = `Expand-Archive -Path '${archivePath.replace(/'/g, "''")}' -DestinationPath '${tmpDir.replace(/'/g, "''")}' -Force` const psResult = spawnSync( "powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", psCmd], { stdio: "pipe", windowsHide: true }, ) if (psResult.status === 0) { extracted = true } } if (!extracted) { const result = spawnSync("unzip", ["-o", archivePath, "-d", tmpDir], { stdio: "pipe" }) if (result.status !== 0) { const unzipErr = result.stderr?.toString().trim() || "command not found" const fflateMsg = fflateError instanceof Error ? fflateError.message : String(fflateError) throw new Error(`zip extraction failed (fflate: ${fflateMsg}; unzip: ${unzipErr})`) } } const srcBinary = path.join(tmpDir, extractedBinary) if (!existsSync(srcBinary)) { throw new Error(`Binary not found at expected path: ${srcBinary}`) } renameSync(srcBinary, binaryPath) } finally { rmSync(tmpDir, { recursive: true, force: true }) } } async function extractTarGz(buffer, binaryPath, extractedBinary, assetName) { const binaryDir = path.dirname(binaryPath) const tmpDir = path.join(binaryDir, ".tmp-download") rmSync(tmpDir, { recursive: true, force: true }) mkdirSync(tmpDir, { recursive: true }) try { const archivePath = path.join(tmpDir, assetName) writeFileSync(archivePath, buffer) const result = spawnSync("tar", ["xzf", archivePath, "-C", tmpDir], { stdio: "pipe" }) if (result.status !== 0) { throw new Error(`tar extract failed: ${result.stderr?.toString()}`) } const srcBinary = path.join(tmpDir, extractedBinary) if (!existsSync(srcBinary)) { throw new Error(`Binary not found at expected path: ${srcBinary}`) } renameSync(srcBinary, binaryPath) } finally { rmSync(tmpDir, { recursive: true, force: true }) } } // --- Main --- async function downloadAndExtract() { const { target, ext } = getPlatformMapping() const assetName = `ripgrep-v${RG_VERSION}-${target}.${ext}` const binaryPath = getBinaryPath() const binaryDir = path.dirname(binaryPath) const force = process.argv.includes("--force") if (!force && existsSync(binaryPath)) { const stat = statSync(binaryPath) if (stat.size > 0) { console.log(`[ripgrep] Binary already exists at ${binaryPath}, skipping.`) return } } console.log(`[ripgrep] Downloading v${RG_VERSION} for ${target}...`) const extractedBinary = process.platform === "win32" ? "rg.exe" : "rg" const mirrors = [RELEASE_BASE] if (RELEASE_BASE === DEFAULT_RELEASE_BASE.replace(/\/$/, "")) { mirrors.push(MIRROR_RELEASE_BASE.replace(/\/$/, "")) } let buffer let lastError for (const base of mirrors) { const url = `${base}/${assetName}` try { console.log(`[ripgrep] Trying ${url}`) buffer = await downloadUrlToBufferWithFallback(url) break } catch (e) { console.warn(`[ripgrep] Download from ${base} failed: ${e instanceof Error ? e.message : e}`) lastError = e } } if (!buffer) { throw lastError } try { console.log(`[ripgrep] Downloaded ${Math.round(buffer.length / 1024)} KB`) mkdirSync(binaryDir, { recursive: true }) if (ext === "tar.gz") { await extractTarGz(buffer, binaryPath, extractedBinary, assetName) } else { await extractZip(buffer, binaryPath, extractedBinary) } if (process.platform !== "win32") { chmodSync(binaryPath, 0o755) } console.log(`[ripgrep] Installed to ${binaryPath}`) } catch (e) { const msg = e instanceof Error ? e.message : String(e) const hint = "Check network or set HTTPS_PROXY. If GitHub is blocked, set RIPGREP_DOWNLOAD_BASE to a mirror (see script header)." throw new Error(`${msg} ${hint}`) } } async function main() { await downloadAndExtract() } main().catch((error) => { const msg = error instanceof Error ? error.message : String(error) console.error(`[postinstall] ripgrep download failed (non-fatal): ${msg}`) console.error(`[postinstall] You can install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`) // Never exit with error code — postinstall must not break install process.exit(0) })