#!/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). Nested `<pkg>/node_modules/<dep>`
* symlinks are NOT flattened, but they are RETARGETED: pgh writes them
* relative (`../../<hash>`), so a copy that moved out of .pgh/<hash>/
* carries links that dangle at its new depth. They all did (1310/1310 on
* stortron, 0.1217.0): every nested require fell through to the top
* level, and keet-core 4.22 got the app's blind-peering 2.1.2 instead of
* its own 2.9.3. Bare caches by realpath, so each nested link must point
* at the ONE place its hash loads from: the flattened top-level copy if
* there is one (a second path would be a second instance), else
* .pgh/<hash>. The same retarget applies to links inside each .pgh/<hash>, so a
* package that stays in .pgh shares the top-level instances too.
*
* 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
let retargeted = 0
// realpath of a .pgh/<hash> dir -> the top-level copy that now loads it
const flattenedFrom = new Map()
// flattened copy -> the .pgh dir it was copied from (its links are relative to there)
const copiedFrom = new Map()
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' })
flattenedFrom.set(real, linkPath)
copiedFrom.set(linkPath, real)
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)
}
}
}
// Every symlink directly in `<pkgDir>/node_modules/` (and `@scope/`).
function nestedLinks (pkgDir) {
const out = []
const nm = path.join(pkgDir, 'node_modules')
let entries
try { entries = fs.readdirSync(nm, { withFileTypes: true }) } catch { return out }
for (const e of entries) {
const p = path.join(nm, e.name)
if (e.isSymbolicLink()) out.push(p)
else if (e.isDirectory() && e.name.startsWith('@')) {
let subs
try { subs = fs.readdirSync(p, { withFileTypes: true }) } catch { continue }
for (const s of subs) if (s.isSymbolicLink()) out.push(path.join(p, s.name))
}
}
return out
}
// Point `link` at where its target hash loads from. `origin` is the dir the
// link's relative text was written against (differs from `pkgDir` for a copy).
function retarget (link, pkgDir, origin) {
const text = fs.readlinkSync(link)
const written = path.join(origin, path.relative(pkgDir, link))
let target = path.resolve(path.dirname(written), text)
try { target = fs.realpathSync(target) } catch { failed++; return }
const canonical = flattenedFrom.get(target) || target
const rel = path.relative(path.dirname(link), canonical)
if (rel === text) return
fs.unlinkSync(link)
fs.symlinkSync(rel, link)
retargeted++
}
function retargetAll () {
for (const [copy, real] of copiedFrom) {
for (const link of nestedLinks(copy)) retarget(link, copy, real)
}
const pgh = path.join(root, '.pgh')
let hashes = []
try { hashes = fs.readdirSync(pgh) } catch {}
for (const h of hashes) {
const dir = path.join(pgh, h)
for (const link of nestedLinks(dir)) retarget(link, dir, dir)
}
}
console.log('flatten-node-modules: scanning ' + root + ' (top-level + scoped)')
scanLevel(root)
retargetAll()
console.log(' replaced: ' + replaced + ' top-level symlinks')
console.log(' retargeted: ' + retargeted + ' nested symlinks')
console.log(' failed: ' + failed)
console.log(' elapsed: ' + ((Date.now() - start) / 1000).toFixed(1) + 's')
console.log(' note: .pgh/ kept in place for nested links to unflattened hashes')