/**
* localFetcher — build a {getTree, getContent} over one or more source
* directories by walking them with the exact same walkTree the
* publisher uses. Feeds installTree so the publisher can populate its
* own `dependencies/` store with every tree it just walked — no
* network round-trip involved.
*
* Critical invariant: the walkTree options here MUST match the options
* used at lockfile-computation time, otherwise the hashes differ and
* installTree throws "invalid tree" on cache misses.
*/
import fs from '#node:fs'
import path from '#node:path'
import { walkTree, KIND_FILE, KIND_SYMLINK } from 'hyper-overlay/tree'
/**
* @param {Array<{ rootPath: string, skip: Set<string> }>} dirs
* @returns {{ getTree: Function, getContent: Function }}
*/
export function localFetcher (dirs) {
const trees = new Map() // treeHash → entries
const content = new Map() // contentHash → Buffer
for (const { rootPath, skip, matcher } of dirs) {
const walked = walkTree(rootPath, { skip, matcher })
for (const [rel, tree] of walked) {
trees.set(tree.hash, tree.entries)
for (const e of tree.entries) {
const absEntry = path.join(rootPath, rel, e.name)
if (e.kind === KIND_FILE) {
try { content.set(e.childHash, fs.readFileSync(absEntry)) } catch {}
} else if (e.kind === KIND_SYMLINK) {
try { content.set(e.childHash, Buffer.from(fs.readlinkSync(absEntry), 'utf8')) } catch {}
}
}
}
}
return {
async getTree (hash) {
const entries = trees.get(hash)
if (!entries) throw new Error('localFetcher miss (tree): ' + hash.slice(0, 12))
const size = entries.reduce((a, e) => a + (e.size || 0), 0)
return { entries, size }
},
async getContent (hash) {
const c = content.get(hash)
if (!c) throw new Error('localFetcher miss (content): ' + hash.slice(0, 12))
return c
}
}
}