OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / flatten-node-modules.cjs

89 lines · javascript ref:
#!/usr/bin/env node
/**
 * Flatten only TOP-LEVEL symlinks in node_modules.
 *
 * Why this specifically: pgh's content-addressed materializer makes
 * `node_modules/<pkg>` and `node_modules/@scope/<pkg>` symlinks into
 * `node_modules/.pgh/<hash>/`. Bundlebee (lazy bundle loader in
 * keet-core 4.x) looks up modules via require.cache, but Bare caches
 * by realpath — so the bundle's `/node_modules/<pkg>/index.js`
 * references never match.
 *
 * Fix: replace each top-level symlink with a real copy of its target
 * (resolved through the symlink). Do NOT recursively flatten nested
 * `node_modules/<pkg>/node_modules/<sub>` symlinks — they can stay
 * pointing into .pgh, because npm-style hoisting means most lookups
 * find their match at the top level anyway. Keep .pgh in place as
 * the fallback for any nested symlink that DOES need resolution.
 *
 * Cycle handling: avoided entirely by only processing top-level
 * entries (one or two levels deep). No recursion into copied content.
 */

'use strict'

const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')

const root = process.argv[2] || 'node_modules'
if (!fs.existsSync(root)) {
  console.error('flatten-node-modules: not found:', root)
  process.exit(1)
}

const start = Date.now()
let replaced = 0
let failed = 0

function flattenLink (linkPath) {
  let real
  try { real = fs.realpathSync(linkPath) }
  catch { failed++; return }
  let st
  try { st = fs.statSync(real) }
  catch { failed++; return }
  if (!st.isDirectory()) {
    // a symlink to a file — just copy the file content
    const data = fs.readFileSync(real)
    fs.unlinkSync(linkPath)
    fs.writeFileSync(linkPath, data)
    replaced++
    return
  }
  // Replace symlink with a real recursive copy. Use `cp -r` (no -L)
  // so nested symlinks inside the target stay as symlinks — they
  // can resolve via the still-existing .pgh/ store.
  fs.unlinkSync(linkPath)
  try {
    execSync('cp -r ' + JSON.stringify(real) + '/. ' + JSON.stringify(linkPath), { stdio: 'pipe' })
    replaced++
  } catch (err) {
    console.error('  cp failed for', linkPath, '←', real, ':', err.message)
    failed++
  }
}

function scanLevel (dir) {
  let entries
  try { entries = fs.readdirSync(dir, { withFileTypes: true }) }
  catch { return }
  for (const entry of entries) {
    if (entry.name === '.pgh') continue
    const p = path.join(dir, entry.name)
    if (entry.isSymbolicLink()) {
      flattenLink(p)
    } else if (entry.isDirectory() && entry.name.startsWith('@')) {
      // scoped package dir — recurse one level for `@scope/<pkg>`
      scanLevel(p)
    }
  }
}

console.log('flatten-node-modules: scanning ' + root + ' (top-level + scoped)')
scanLevel(root)
console.log('  replaced:    ' + replaced + ' top-level symlinks')
console.log('  failed:      ' + failed)
console.log('  elapsed:     ' + ((Date.now() - start) / 1000).toFixed(1) + 's')
console.log('  note:        .pgh/ kept in place for nested-symlink fallback')