Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env tsx
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const DEFAULT_DOCS_ROOTS = ["README.md", "AGENTS.md", "SECURITY.md", "CHANGELOG.md", "docs", "guides", "llm-docs"];
|
||||
|
||||
const MARKDOWN_LINK_PATTERN = /!?\[([^\]\n]*(?:\][^\]\n]*)*)\]\(([^)\n]+)\)/g;
|
||||
const HEADING_PATTERN = /^#{1,6}\s+(.+)$/gm;
|
||||
|
||||
function normalizeRelativePath(relativePath: string): string {
|
||||
return relativePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function lineAndColumnForIndex(content: string, index: number): { line: number; column: number } {
|
||||
const prefix = content.slice(0, index);
|
||||
const lines = prefix.split(/\r?\n/);
|
||||
return { line: lines.length, column: (lines.at(-1) ?? "").length + 1 };
|
||||
}
|
||||
|
||||
function collectMarkdownFiles(rootDir: string, docsRoots = DEFAULT_DOCS_ROOTS): { files: string[]; missingRoots: string[] } {
|
||||
const files: string[] = [];
|
||||
const missingRoots: string[] = [];
|
||||
|
||||
function visit(absolutePath: string): void {
|
||||
const stat = fs.statSync(absolutePath);
|
||||
if (stat.isDirectory()) {
|
||||
for (const entry of fs.readdirSync(absolutePath).sort()) {
|
||||
visit(path.join(absolutePath, entry));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (/\.mdx?$/i.test(absolutePath)) {
|
||||
files.push(normalizeRelativePath(path.relative(rootDir, absolutePath)));
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of docsRoots) {
|
||||
const absolutePath = path.join(rootDir, root);
|
||||
if (fs.existsSync(absolutePath)) {
|
||||
visit(absolutePath);
|
||||
} else {
|
||||
missingRoots.push(root);
|
||||
}
|
||||
}
|
||||
|
||||
return { files, missingRoots };
|
||||
}
|
||||
|
||||
function slugifyHeading(heading: string): string {
|
||||
return heading
|
||||
.replace(/`([^`]+)`/g, "$1")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-");
|
||||
}
|
||||
|
||||
function anchorsFor(content: string): Set<string> {
|
||||
const anchors = new Set<string>();
|
||||
const seen = new Map<string, number>();
|
||||
for (const match of content.matchAll(HEADING_PATTERN)) {
|
||||
const raw = match[1].replace(/#+\s*$/, "").trim();
|
||||
const base = slugifyHeading(raw);
|
||||
if (!base) continue;
|
||||
const count = seen.get(base) ?? 0;
|
||||
seen.set(base, count + 1);
|
||||
anchors.add(count === 0 ? base : `${base}-${count}`);
|
||||
}
|
||||
return anchors;
|
||||
}
|
||||
|
||||
function isExternalOrSpecialTarget(target: string): boolean {
|
||||
return /^(?:[a-z][a-z0-9+.-]*:|#|mailto:|tel:)/i.test(target);
|
||||
}
|
||||
|
||||
function stripQueryAndHash(target: string): { targetPath: string; anchor: string } {
|
||||
const hashIndex = target.indexOf("#");
|
||||
const beforeHash = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
|
||||
const anchor = hashIndex >= 0 ? decodeURIComponent(target.slice(hashIndex + 1)) : "";
|
||||
const queryIndex = beforeHash.indexOf("?");
|
||||
const targetPath = queryIndex >= 0 ? beforeHash.slice(0, queryIndex) : beforeHash;
|
||||
return { targetPath, anchor };
|
||||
}
|
||||
|
||||
export function checkDocsLinks(rootDir = process.cwd(), docsRoots = DEFAULT_DOCS_ROOTS): { failures: string[]; checkedFiles: string[] } {
|
||||
const { files: checkedFiles, missingRoots } = collectMarkdownFiles(rootDir, docsRoots);
|
||||
const contentByPath = new Map<string, string>();
|
||||
const anchorByPath = new Map<string, Set<string>>();
|
||||
|
||||
for (const relativePath of checkedFiles) {
|
||||
const content = fs.readFileSync(path.join(rootDir, relativePath), "utf8");
|
||||
contentByPath.set(relativePath, content);
|
||||
anchorByPath.set(relativePath, anchorsFor(content));
|
||||
}
|
||||
|
||||
const failures: string[] = [];
|
||||
|
||||
for (const missingRoot of missingRoots) {
|
||||
failures.push(`docs-roots: missing configured docs root ${missingRoot}`);
|
||||
}
|
||||
|
||||
if (checkedFiles.length === 0) {
|
||||
failures.push(`docs-roots: checked 0 Markdown files for roots ${docsRoots.join(", ")}`);
|
||||
return { failures, checkedFiles };
|
||||
}
|
||||
|
||||
for (const [sourcePath, content] of contentByPath.entries()) {
|
||||
for (const match of content.matchAll(MARKDOWN_LINK_PATTERN)) {
|
||||
const rawTarget = match[2].trim();
|
||||
if (!rawTarget || isExternalOrSpecialTarget(rawTarget)) continue;
|
||||
|
||||
const { targetPath, anchor } = stripQueryAndHash(rawTarget);
|
||||
const sourcePosition = lineAndColumnForIndex(content, match.index ?? 0);
|
||||
const sourceContext = `${sourcePath}:${sourcePosition.line}:${sourcePosition.column}`;
|
||||
const resolvedPath = targetPath
|
||||
? normalizeRelativePath(
|
||||
path.normalize(
|
||||
targetPath.startsWith("/")
|
||||
? path.join("docs", decodeURIComponent(targetPath).slice(1))
|
||||
: path.join(path.dirname(sourcePath), decodeURIComponent(targetPath)),
|
||||
),
|
||||
)
|
||||
: sourcePath;
|
||||
const absoluteTarget = path.join(rootDir, resolvedPath);
|
||||
|
||||
const targetExists = !targetPath || fs.existsSync(absoluteTarget);
|
||||
const markdownExtension = path.extname(resolvedPath);
|
||||
const extensionlessDocExists =
|
||||
targetPath && !targetExists && markdownExtension === "" && fs.existsSync(`${absoluteTarget}.md`);
|
||||
|
||||
if (targetPath && !targetExists && !extensionlessDocExists) {
|
||||
failures.push(`${sourceContext}: broken local link "${rawTarget}" resolves to missing path ${resolvedPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const finalAbsoluteTarget = extensionlessDocExists ? `${absoluteTarget}.md` : absoluteTarget;
|
||||
const finalResolvedPath = extensionlessDocExists ? `${resolvedPath}.md` : resolvedPath;
|
||||
|
||||
if (targetPath && fs.statSync(finalAbsoluteTarget).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (anchor) {
|
||||
const targetContent = targetPath ? fs.readFileSync(finalAbsoluteTarget, "utf8") : content;
|
||||
const targetAnchors = targetPath ? anchorsFor(targetContent) : anchorByPath.get(sourcePath) ?? new Set<string>();
|
||||
if (!targetAnchors.has(anchor)) {
|
||||
failures.push(`${sourceContext}: broken local anchor "#${anchor}" in ${finalResolvedPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { failures, checkedFiles };
|
||||
}
|
||||
|
||||
export function formatDocsLinkCheckResult(result: { failures: string[]; checkedFiles: string[] }): string {
|
||||
const status = result.failures.length > 0 ? "FAIL" : "PASS";
|
||||
return [
|
||||
`${status} docs link check`,
|
||||
`OBSERVE checked ${result.checkedFiles.length} Markdown files`,
|
||||
...result.failures.map((failure) => `FAIL ${failure}`),
|
||||
`Summary: ${result.failures.length === 0 ? 1 : 0} passed, 1 observed, ${result.failures.length} failed`,
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
||||
|
||||
if (isCli) {
|
||||
const result = checkDocsLinks(process.argv[2] ? path.resolve(process.argv[2]) : process.cwd());
|
||||
const output = formatDocsLinkCheckResult(result);
|
||||
if (result.failures.length > 0) {
|
||||
process.stderr.write(output);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.stdout.write(output);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user