OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / scripts / lib / prune-nested.js

168 lines · javascript ref:
/**
 * prune-nested — the bare-bootstrap build's "smart" nested-node_modules prune.
 *
 * Shared by pgh's and pgh-sidecar's build-bootstrap-bare.js (extracted so the
 * logic lives in one place and can be unit-tested — the build scripts run
 * top-level side effects on import, so they can't be imported directly).
 *
 * Goal: a bare tarball copies a package's FULL node_modules, which after npm
 * hoisting contains many `node_modules/<X>/node_modules/<dep>` copies. Some
 * are true hoist-duplicates (the same version also sits higher up and would
 * resolve identically); deleting those shrinks the bundle. Others are
 * load-bearing version-conflict resolutions that MUST stay.
 *
 * Correctness hinges on what a nested copy would resolve to AFTER deletion.
 * Node/Bare resolve by walking node_modules ancestors NEAREST-first. So a
 * nested `<dep>@V` is a true hoist-dup iff its NEAREST ancestor already
 * provides `<dep>@V` — NOT merely "the bundle root has V". The earlier
 * version compared against the root only, which deleted copies that an
 * INTERMEDIATE node_modules would shadow with a DIFFERENT version. Concretely
 * it deleted pgh/node_modules/hypercore/node_modules/compact-encoding@3.2.0
 * when the bundle root also had ce@3.2.0 — but hypercore then fell through to
 * pgh/node_modules/compact-encoding@2.19.2 (the intermediate), and
 * hypercore@11.33.1 (needs ce ^3.0.0) booted against ce 2.x → DECODING_ERROR.
 *
 * The fix threads a per-level resolution map (`resolveHere`): the merge of a
 * directory's node_modules ancestors overlaid with its own packages
 * (nearest wins). A nested copy is deleted only when `resolveHere` for the
 * OWNING package already maps that dep to the SAME version.
 */

import fs from '#node:fs' // #node:* resolves to bare-fs under bare, fs under node — test:bare imports this
import path from '#node:path'

/**
 * Index a single node_modules dir → Map<name, version> for the packages that
 * live DIRECTLY in it (handles `@scope/name`). Used both for the top-level
 * index and to build each nested level's resolution map.
 */
export function indexTopLevel (nodeModulesDir) {
  const index = new Map()
  function readVersion (pkgDir, name) {
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'))
      if (pkg && pkg.version) index.set(name, pkg.version)
    } catch {}
  }
  let entries
  try { entries = fs.readdirSync(nodeModulesDir) } catch { return index }
  for (const name of entries) {
    if (name.startsWith('.')) continue
    const pkgDir = path.join(nodeModulesDir, name)
    let stat
    try { stat = fs.lstatSync(pkgDir) } catch { continue }
    if (!stat.isDirectory()) continue
    if (name.startsWith('@')) {
      let subs
      try { subs = fs.readdirSync(pkgDir) } catch { continue }
      for (const sub of subs) {
        const subDir = path.join(pkgDir, sub)
        let subStat; try { subStat = fs.statSync(subDir) } catch { continue }
        if (!subStat.isDirectory()) continue
        readVersion(subDir, name + '/' + sub)
      }
    } else {
      readVersion(pkgDir, name)
    }
  }
  return index
}

/**
 * Prune true hoist-duplicate nested copies under `topNodeModules`, in place.
 * @returns {{ dupsRemoved: number, nestedKept: number }}
 */
export function pruneNestedSmart (topNodeModules) {
  let dupsRemoved = 0
  let nestedKept = 0

  // resolveHere: Map<name, version> — what a package located directly in this
  // node_modules resolves each dep to when it has no own nested copy (merge of
  // ancestors + this level's own packages, nearest wins). This is exactly the
  // version a nested copy would fall through to if deleted.
  function visit (nmDir, resolveHere) {
    let entries
    try { entries = fs.readdirSync(nmDir) } catch { return }
    for (const name of entries) {
      if (name.startsWith('.')) continue
      const pkgDir = path.join(nmDir, name)
      let stat
      try { stat = fs.lstatSync(pkgDir) } catch { continue }
      if (stat.isSymbolicLink() || !stat.isDirectory()) continue

      if (name.startsWith('@')) {
        let subs
        try { subs = fs.readdirSync(pkgDir) } catch { continue }
        for (const sub of subs) processPackage(pkgDir, sub, resolveHere)
      } else {
        processPackage(nmDir, name, resolveHere)
      }
    }
  }

  function processPackage (parentDir, dirName, resolveHere) {
    const pkgDir = path.join(parentDir, dirName)
    const nested = path.join(pkgDir, 'node_modules')
    if (!fs.existsSync(nested)) return

    // Resolution map for THIS package's private deps (those living in
    // `nested`): they see `nested` nearest, then resolveHere upward. Snapshot
    // before any removal — a removed copy matches resolveHere, so the
    // effective version is unchanged whether read from it or the ancestor.
    const childResolve = new Map(resolveHere)
    for (const [n, v] of indexTopLevel(nested)) childResolve.set(n, v)

    // Keep/delete each copy in `nested` using the OWNING package's resolution
    // (resolveHere) — what it falls through to if the copy is removed.
    let inner
    try { inner = fs.readdirSync(nested) } catch { return }
    for (const innerName of inner) {
      if (innerName.startsWith('.')) continue
      const innerDir = path.join(nested, innerName)
      let innerStat
      try { innerStat = fs.lstatSync(innerDir) } catch { continue }
      if (innerStat.isSymbolicLink() || !innerStat.isDirectory()) continue

      if (innerName.startsWith('@')) {
        let subs
        try { subs = fs.readdirSync(innerDir) } catch { continue }
        for (const sub of subs) maybeRemove(innerDir, sub, innerName + '/' + sub, resolveHere)
      } else {
        maybeRemove(nested, innerName, innerName, resolveHere)
      }
    }

    // Recurse: kept private deps may have their OWN nested deps.
    visit(nested, childResolve)

    // Clean up empty node_modules dir.
    try {
      if (fs.existsSync(nested) && fs.readdirSync(nested).length === 0) {
        fs.rmSync(nested, { recursive: true, force: true })
      }
    } catch {}
  }

  function maybeRemove (parentDir, dirName, fullName, resolveHere) {
    const pkgPath = path.join(parentDir, dirName)
    let version
    try {
      version = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf8')).version
    } catch { return }
    const resolvedVersion = resolveHere.get(fullName)
    if (resolvedVersion && resolvedVersion === version) {
      // Nearest ancestor already provides this EXACT version — removing the
      // nested copy resolves to the same version. True hoist-dup.
      fs.rmSync(pkgPath, { recursive: true, force: true })
      dupsRemoved++
    } else {
      // Nearest ancestor has a DIFFERENT version (or none) — this nested copy
      // is load-bearing (version-conflict resolution). Keep it.
      nestedKept++
    }
  }

  visit(topNodeModules, indexTopLevel(topNodeModules))
  return { dupsRemoved, nestedKept }
}