OverviewFilesCommitsIssuesDocsReleasesPeers

wirecore / pear-app / prune-prebuilds.js

210 lines · javascript ref:
#!/usr/bin/env node
/**
 * Prune platform-specific prebuilds from node_modules
 *
 * This removes prebuilds for platforms other than the current one,
 * reducing package size from ~300MB to ~40MB (85% reduction)
 *
 * Run after: npm install
 * Run before: pear stage
 */

const fs = require('fs')
const path = require('path')
const os = require('os')

// Detect current platform
const platform = os.platform() // 'linux', 'darwin', 'win32'
const arch = os.arch()          // 'x64', 'arm64', etc.
const currentPlatform = `${platform}-${arch}`

console.log(`🔍 Current platform: ${currentPlatform}`)
console.log(`🗑️  Removing prebuilds for other platforms...\n`)

let totalRemoved = 0
let totalKept = 0
let packagesProcessed = 0

/**
 * Recursively find and prune prebuilds directories
 */
function pruneDirectory(dir, depth = 0) {
  if (depth > 10) return // Prevent infinite recursion

  try {
    const entries = fs.readdirSync(dir, { withFileTypes: true })

    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name)

      if (entry.isDirectory()) {
        if (entry.name === 'prebuilds') {
          // Found a prebuilds directory - prune it
          prunePrebuild(fullPath)
          packagesProcessed++
        } else if (entry.name === 'node_modules') {
          // Recurse into nested node_modules
          pruneDirectory(fullPath, depth + 1)
        } else if (entry.name.startsWith('@')) {
          // Scoped package namespace - recurse into it
          pruneDirectory(fullPath, depth)
        } else {
          // Check every directory for prebuilds and nested node_modules
          const prebuildsPath = path.join(fullPath, 'prebuilds')
          if (fs.existsSync(prebuildsPath)) {
            prunePrebuild(prebuildsPath)
            packagesProcessed++
          }
          // Also check nested node_modules
          const nestedNodeModules = path.join(fullPath, 'node_modules')
          if (fs.existsSync(nestedNodeModules)) {
            pruneDirectory(nestedNodeModules, depth + 1)
          }
        }
      }
    }
  } catch (err) {
    // Ignore permission errors, etc.
    console.error(`⚠️  Error reading ${dir}: ${err.message}`)
  }
}

/**
 * Recursively remove directory (compatible with Node v10+)
 */
function removeDir(dir) {
  if (!fs.existsSync(dir)) return

  const entries = fs.readdirSync(dir, { withFileTypes: true })

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name)

    if (entry.isDirectory()) {
      removeDir(fullPath)
    } else {
      fs.unlinkSync(fullPath)
    }
  }

  fs.rmdirSync(dir)
}

/**
 * Prune a specific prebuilds directory
 */
function prunePrebuild(prebuildsPath) {
  try {
    const platforms = fs.readdirSync(prebuildsPath)
    const packageName = path.basename(path.dirname(prebuildsPath))

    let removedPlatforms = []
    let keptPlatforms = []

    for (const platform of platforms) {
      const platformPath = path.join(prebuildsPath, platform)

      let stat
      try {
        stat = fs.statSync(platformPath)
      } catch (err) {
        continue
      }

      if (!stat.isDirectory()) continue

      if (platform === currentPlatform) {
        // Keep current platform
        keptPlatforms.push(platform)
        totalKept++
      } else {
        // Remove other platforms
        removeDir(platformPath)
        removedPlatforms.push(platform)
        totalRemoved++
      }
    }

    if (removedPlatforms.length > 0) {
      console.log(`📦 ${packageName}:`)
      console.log(`   ✅ Kept: ${keptPlatforms.join(', ') || 'none'}`)
      console.log(`   ❌ Removed: ${removedPlatforms.join(', ')}`)
    }
  } catch (err) {
    console.error(`⚠️  Error processing ${prebuildsPath}: ${err.message}`)
  }
}

/**
 * Get directory size
 */
function getDirSize(dir) {
  let size = 0

  try {
    const entries = fs.readdirSync(dir, { withFileTypes: true })

    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name)

      if (entry.isDirectory()) {
        size += getDirSize(fullPath)
      } else {
        const stat = fs.statSync(fullPath)
        size += stat.size
      }
    }
  } catch (err) {
    // Ignore errors
  }

  return size
}

/**
 * Format bytes to human-readable
 */
function formatBytes(bytes) {
  if (bytes < 1024) return bytes + ' B'
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}

// Main execution
const nodeModulesPath = path.join(__dirname, 'node_modules')

if (!fs.existsSync(nodeModulesPath)) {
  console.error('❌ node_modules not found. Run npm install first.')
  process.exit(1)
}

// Measure before
const sizeBefore = getDirSize(nodeModulesPath)
console.log(`📊 Size before: ${formatBytes(sizeBefore)}\n`)

// Prune
const startTime = Date.now()
pruneDirectory(nodeModulesPath)
const duration = ((Date.now() - startTime) / 1000).toFixed(2)

// Measure after
const sizeAfter = getDirSize(nodeModulesPath)
const saved = sizeBefore - sizeAfter
const percentSaved = ((saved / sizeBefore) * 100).toFixed(1)

// Summary
console.log(`\n✅ Pruning complete in ${duration}s`)
console.log(`\n📊 Summary:`)
console.log(`   Packages processed: ${packagesProcessed}`)
console.log(`   Platforms kept: ${totalKept}`)
console.log(`   Platforms removed: ${totalRemoved}`)
console.log(`   Size before: ${formatBytes(sizeBefore)}`)
console.log(`   Size after: ${formatBytes(sizeAfter)}`)
console.log(`   Saved: ${formatBytes(saved)} (${percentSaved}%)`)

if (saved > 1024 * 1024) {
  console.log(`\n🎉 Saved ${formatBytes(saved)}! Ready for staging.`)
} else {
  console.log(`\n⚠️  Minimal savings. Prebuilds may already be pruned.`)
}