(
root: string,
files: Array<{ rel: string; content?: string }>,
read?: (rel: string) => Promise<string | null>,
)
| 155 | |
| 156 | /** Build the import graph for a set of files. `read` lets callers inject cached content. */ |
| 157 | export async function buildImportGraph( |
| 158 | root: string, |
| 159 | files: Array<{ rel: string; content?: string }>, |
| 160 | read?: (rel: string) => Promise<string | null>, |
| 161 | ): Promise<ImportGraph> { |
| 162 | const fileSet = new Set(files.map(f => f.rel)); |
| 163 | const basenameIndex = new Map<string, string[]>(); |
| 164 | for (const f of fileSet) { |
| 165 | const base = path.basename(f).replace(path.extname(f), ''); |
| 166 | const arr = basenameIndex.get(base) ?? []; |
| 167 | arr.push(f); |
| 168 | basenameIndex.set(base, arr); |
| 169 | } |
| 170 | |
| 171 | const out = new Map<string, Set<string>>(); |
| 172 | const inMap = new Map<string, Set<string>>(); |
| 173 | for (const f of fileSet) { out.set(f, new Set()); inMap.set(f, new Set()); } |
| 174 | |
| 175 | for (const f of files) { |
| 176 | let content = f.content; |
| 177 | if (content == null) { |
| 178 | content = read ? await read(f.rel) ?? undefined : await fs.readFile(path.join(root, f.rel), 'utf-8').catch(() => undefined); |
| 179 | } |
| 180 | if (!content) continue; |
| 181 | const specs = extractImportSpecifiers(content, detectLang(f.rel)); |
| 182 | for (const spec of specs) { |
| 183 | const target = resolveSpecifier(spec, f.rel, fileSet, basenameIndex); |
| 184 | if (target && target !== f.rel) { |
| 185 | out.get(f.rel)!.add(target); |
| 186 | inMap.get(target)!.add(f.rel); |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | return { out, in: inMap, files: fileSet }; |
| 192 | } |
| 193 | |
| 194 | /** |
| 195 | * Expand a seed set of files by following import edges up to `hops` hops in BOTH |
no test coverage detected