Files
brk/scripts/build-ask-source.mjs
T
2026-07-25 22:18:21 +02:00

163 lines
4.4 KiB
JavaScript

import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { gzipSync } from "node:zlib";
const repository = "bitcoinresearchkit/brk";
const revision = process.argv[2] ?? "main";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const output = resolve(
process.argv[3] ?? `${root}/website_next/ask/tools/source/catalog.jsonl.gz`,
);
const temporaryOutput = `${output}.${process.pid}.tmp`;
const maxFileSize = 128_000;
const concurrency = 12;
const extensions = new Set([
"css",
"html",
"js",
"json",
"md",
"mjs",
"py",
"rs",
"sh",
"toml",
"ts",
"yaml",
"yml",
]);
const excludedPrefixes = [
".git/",
".github/",
"docs/ai/",
"modules/",
"packages/brk_client/brk_client/",
"target/",
"website/assets/",
"website_next/modules/",
];
const excludedFiles = new Set([
"crates/brk_server/src/api/scalar.js",
"docs/CHANGELOG.md",
"website/scripts/options/scalar.js",
]);
const headers = {
Accept: "application/vnd.github+json",
"User-Agent": "brk-ask-source-builder",
};
async function fetchResponse(url, options = {}) {
let lastError;
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
lastError = new Error(`${response.status} ${response.statusText}: ${url}`);
if (response.status < 500 && response.status !== 429) break;
} catch (error) {
lastError = error;
}
await new Promise((resolveDelay) =>
setTimeout(resolveDelay, 250 * 2 ** attempt),
);
}
throw lastError;
}
async function fetchJson(url) {
return fetchResponse(url, { headers }).then((response) => response.json());
}
function isUsefulSource(entry) {
if (entry.type !== "blob" || !entry.size || entry.size > maxFileSize) {
return false;
}
const extension = entry.path.slice(entry.path.lastIndexOf(".") + 1).toLowerCase();
return (
extensions.has(extension) &&
!excludedFiles.has(entry.path) &&
!excludedPrefixes.some((prefix) => entry.path.startsWith(prefix))
);
}
function rawUrl(commit, filePath) {
const encodedPath = filePath.split("/").map(encodeURIComponent).join("/");
return `https://raw.githubusercontent.com/${repository}/${commit}/${encodedPath}`;
}
async function mapConcurrent(items, mapper) {
const results = new Array(items.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < items.length) {
const index = nextIndex;
nextIndex += 1;
results[index] = await mapper(items[index], index);
}
}
await Promise.all(
Array.from({ length: Math.min(concurrency, items.length) }, worker),
);
return results;
}
async function main() {
const commitInfo = await fetchJson(
`https://api.github.com/repos/${repository}/commits/${encodeURIComponent(revision)}`,
);
const commit = commitInfo.sha;
if (!commit) throw new Error(`Could not resolve revision: ${revision}`);
const treeInfo = await fetchJson(
`https://api.github.com/repos/${repository}/git/trees/${commit}?recursive=1`,
);
if (treeInfo.truncated) {
throw new Error("GitHub returned a truncated repository tree");
}
const entries = treeInfo.tree.filter(isUsefulSource);
entries.sort((left, right) => left.path.localeCompare(right.path));
const files = await mapConcurrent(entries, async (entry, index) => {
const response = await fetchResponse(rawUrl(commit, entry.path));
const source = await response.text();
if ((index + 1) % 250 === 0 || index + 1 === entries.length) {
console.error(`Fetched ${index + 1}/${entries.length}`);
}
return JSON.stringify([entry.path, source]);
});
const header = JSON.stringify({
repository,
revision: commit,
count: files.length,
});
const raw = Buffer.from([header, ...files].join("\n"));
const compressed = gzipSync(raw, { level: 9 });
await mkdir(dirname(output), { recursive: true });
try {
await writeFile(temporaryOutput, compressed);
await rename(temporaryOutput, output);
} catch (error) {
await unlink(temporaryOutput).catch(() => {});
throw error;
}
console.log(
JSON.stringify(
{
revision: commit,
files: files.length,
rawBytes: raw.length,
compressedBytes: compressed.length,
output,
},
null,
2,
),
);
}
await main();