import path from 'path'; import { ResolverIO } from '@beehexa/hexasync-template-compose'; /** * Build a hermetic, in-memory ResolverIO from a virtual filesystem map * (absolute POSIX path -> file text). No real disk, no reads of external * template repos (NFR6). Keys should be absolute paths under a synthetic root. */ export function makeVirtualIO(files: Record): ResolverIO { const norm = (p: string) => path.posix.normalize(p); const fs = new Map(); for (const [k, v] of Object.entries(files)) fs.set(norm(k), v); // Async to match the port (AD-11), even though a Map lookup could answer synchronously. // A sync test double against an async port is exactly how a half-async chain hides: the // fixture would resolve immediately and mask a missing `await` in production code. return { canonical: async (p: string) => norm(p), readFileIfExists: async (p: string) => fs.has(norm(p)) ? fs.get(norm(p))! : null, globYaml: async (componentPath: string) => { const base = norm(componentPath).replace(/\/$/, '') + '/'; return [...fs.keys()] .filter((k) => k.startsWith(base) && k.endsWith('.yaml')) .sort(); }, searchMain: async (folder: string) => { const base = norm(folder).replace(/\/$/, '') + '/'; const mains = [...fs.keys()].filter( (k) => (k.startsWith(base) || k === norm(folder)) && k.endsWith('/main.yaml'), ); if (mains.length === 0) throw new Error(`There is no main.yaml file in the project.`); if (mains.length > 1) throw new Error( `Looks like there are more than one main.yaml file in the project.`, ); return { mainYml: fs.get(mains[0])!, componentPath: path.posix.dirname(mains[0]), }; }, }; }