import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { Kind, parse, type DefinitionNode, type DocumentNode, type FragmentDefinitionNode, } from 'graphql'; import { safeListFilesRecursive } from './fs-util'; /** Load every `fragment X on Y { ... }` definition from standalone `.graphql`/`.gql` files under `dirs`. */ export function loadFragmentDefinitionsFromDirs( dirs: readonly string[], repoRoot: string ): Map { const map = new Map(); for (const dir of dirs) { const absDir = join(repoRoot, dir); for (const file of safeListFilesRecursive(absDir, ['.graphql', '.gql'])) { const doc = parse(readFileSync(file, 'utf8')); for (const def of doc.definitions) { if (def.kind === Kind.FRAGMENT_DEFINITION) { map.set(def.name.value, def); } } } } return map; } /** Collect the transitive set of fragment names spread anywhere within a set of definitions. */ export function collectSpreadNames( definitions: readonly DefinitionNode[], found: Set = new Set() ): Set { const visit = (node: unknown): void => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { for (const item of node) visit(item); return; } const obj = node as { kind?: string; name?: { value?: string } }; if (obj.kind === Kind.FRAGMENT_SPREAD && obj.name?.value) { found.add(obj.name.value); } for (const key of Object.keys(obj)) { if (key === 'loc') continue; visit((obj as Record)[key]); } }; for (const def of definitions) visit(def); return found; } /** * Given a parsed document and a pool of known fragment definitions (from * inline `fragment` blocks and/or standalone fragment files), return a new * document with every transitively-spread fragment's definition appended. * A spread whose name isn't found in `fragmentPool` is left unresolved — * `validate()` will correctly report it as an "Unknown fragment" error. */ export function resolveFragments( doc: DocumentNode, fragmentPool: ReadonlyMap ): DocumentNode { const needed = new Set(); let frontier = collectSpreadNames(doc.definitions); while (frontier.size > 0) { const next = new Set(); for (const name of frontier) { if (needed.has(name)) continue; needed.add(name); const fragDef = fragmentPool.get(name); if (fragDef) { for (const nested of collectSpreadNames([fragDef])) { if (!needed.has(nested)) next.add(nested); } } } frontier = next; } const extraDefs: DefinitionNode[] = []; for (const name of needed) { const fragDef = fragmentPool.get(name); if (fragDef) extraDefs.push(fragDef); } if (extraDefs.length === 0) return doc; return { ...doc, definitions: [...doc.definitions, ...extraDefs] }; }