import { readFile, stat } from "node:fs/promises";
import { XMLParser } from "fast-xml-parser";
import type { Nodes } from "mdast";
import { fromMarkdown } from "mdast-util-from-markdown";
import pMap from "p-map";
import { join, relative } from "pathe";
import { glob } from "tinyglobby";
import { examplesRouteBase } from "../astro/templates.ts";
import { stripBasePath } from "../core/base-path.ts";
import type { BlumeManifest, RouteManifestEntry } from "../core/types.ts";
import { buildSnapshot } from "./snapshot.ts";
import type { LlmsDoc, PageSnapshot, RobotsDoc, SitemapDoc } from "./types.ts";
/**
* The route prefix `` preview frames live under. They are bare
* documents rendered for iframes — deliberately noindex, no title worth
* grading, no front matter to fix — so auditing them as pages only produces
* findings nobody can act on.
*/
const EXAMPLES_PREFIX = `${examplesRouteBase("")}/`;
/**
* Ceiling on concurrent file reads/stats while crawling. Unbounded fan-out
* over a large `dist` risks EMFILE and holds every page's HTML in memory at
* once.
*/
const CRAWL_CONCURRENCY = 16;
/** Everything read off disk in one pass over the built site. */
export interface CrawlResult {
pages: PageSnapshot[];
/** Every file in the static dir: URL path -> size in bytes. */
files: Map;
sitemap: SitemapDoc | null;
robots: RobotsDoc | null;
llms: LlmsDoc | null;
}
/**
* The URL a built HTML file is served at. Astro's directory build format emits
* `docs/api/index.html` for `/docs/api`, so the `index.html` leaf collapses;
* a flat `404.html` keeps its name.
*/
export const fileToUrl = (staticDir: string, file: string): string => {
const rel = relative(staticDir, file).replaceAll("\\", "/");
const path = rel.replace(/(?:^|\/)index\.html$/u, "").replace(/\.html$/u, "");
return path === "" ? "/" : `/${path}`;
};
/**
* Every file in the static dir, keyed the way an HTML `src`/`href` would name
* it, with its size — so a reference can be resolved and weighed in one pass.
*/
const indexFiles = async (staticDir: string): Promise