mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-05 15:13:06 -07:00
163 lines
4.4 KiB
JavaScript
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();
|