#!/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')