266 lines
9.4 KiB
JavaScript
266 lines
9.4 KiB
JavaScript
const http = require("http");
|
|
const { readFile, stat } = require("fs");
|
|
const https = require("https");
|
|
const { extname, join, normalize } = require("path");
|
|
|
|
const port = process.env.PORT || 3000;
|
|
const public_dir = join(__dirname, "public");
|
|
const mime_types = {
|
|
".html": "text/html; charset=UTF-8",
|
|
".css": "text/css; charset=UTF-8",
|
|
".js": "application/javascript; charset=UTF-8",
|
|
".json": "application/json; charset=UTF-8",
|
|
".svg": "image/svg+xml",
|
|
".woff": "font/woff",
|
|
".woff2": "font/woff2",
|
|
".ttf": "font/ttf",
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".ico": "image/x-icon"
|
|
};
|
|
|
|
const github_sources_by_track = {
|
|
frontend: [
|
|
{ repo: "vuejs/core", ref: "main", files: ["packages/reactivity/src/effect.ts", "packages/reactivity/src/ref.ts"] },
|
|
{ repo: "twbs/bootstrap", ref: "main", files: ["js/src/modal.js", "js/src/collapse.js"] },
|
|
{ repo: "vercel/next.js", ref: "canary", files: ["packages/next/src/client/app-router.ts", "packages/next/src/client/components/router-reducer/router-reducer.ts"] },
|
|
{ repo: "microsoft/TypeScript", ref: "main", files: ["src/services/services.ts", "src/compiler/utilities.ts"] }
|
|
],
|
|
backend: [
|
|
{ repo: "nodejs/node", ref: "main", files: ["lib/fs.js", "lib/http.js"] },
|
|
{ repo: "python/cpython", ref: "main", files: ["Lib/pathlib.py", "Lib/http/server.py"] },
|
|
{ repo: "golang/go", ref: "master", files: ["src/net/http/server.go", "src/database/sql/sql.go"] },
|
|
{ repo: "laravel/framework", ref: "master", files: ["src/Illuminate/Routing/Router.php", "src/Illuminate/Database/Query/Builder.php"] }
|
|
],
|
|
systems: [
|
|
{ repo: "rust-lang/rust", ref: "master", files: ["library/std/src/fs.rs", "library/std/src/path.rs"] },
|
|
{ repo: "redis/redis", ref: "unstable", files: ["src/server.c", "src/networking.c"] },
|
|
{ repo: "torvalds/linux", ref: "master", files: ["kernel/sched/core.c", "kernel/fork.c"] },
|
|
{ repo: "llvm/llvm-project", ref: "main", files: ["llvm/lib/Support/CommandLine.cpp", "llvm/lib/Support/Path.cpp"] }
|
|
],
|
|
"full-stack": [
|
|
{ repo: "vercel/next.js", ref: "canary", files: ["packages/next/src/server/base-server.ts", "packages/next/src/client/app-router.ts"] },
|
|
{ repo: "remix-run/remix", ref: "main", files: ["packages/remix-server-runtime/server.ts", "packages/remix-react/components.tsx"] },
|
|
{ repo: "microsoft/TypeScript", ref: "main", files: ["src/compiler/checker.ts", "src/compiler/watch.ts"] },
|
|
{ repo: "nodejs/node", ref: "main", files: ["lib/path.js", "lib/http.js"] }
|
|
]
|
|
};
|
|
|
|
const all_tracks = Object.values(github_sources_by_track).flat();
|
|
const sanitize_track = (value) => {
|
|
if (github_sources_by_track[value]) return value;
|
|
if (value === "easy") return "frontend";
|
|
if (value === "normal") return "backend";
|
|
if (value === "hard" || value === "systems programming" || value === "systems-programming") return "systems";
|
|
if (value === "expert" || value === "fullstack") return "full-stack";
|
|
return "full-stack";
|
|
};
|
|
|
|
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
const count_indent = (line) => (line.match(/^\s*/)?.[0].length || 0);
|
|
|
|
const fetch_text = (url) =>
|
|
new Promise((resolve, reject) => {
|
|
https
|
|
.get(url, { headers: { "User-Agent": "typeup-snippet-fetcher" } }, (res) => {
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
fetch_text(res.headers.location).then(resolve).catch(reject);
|
|
return;
|
|
}
|
|
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`status ${res.statusCode}`));
|
|
return;
|
|
}
|
|
|
|
let body = "";
|
|
res.setEncoding("utf8");
|
|
res.on("data", (chunk) => {
|
|
body += chunk;
|
|
});
|
|
res.on("end", () => resolve(body));
|
|
})
|
|
.on("error", reject);
|
|
});
|
|
|
|
const extension_from_source = (source) => (source.split(".").pop() || "").toLowerCase();
|
|
|
|
const normalize_lines = (text) => text.split("\n").map((line) => line.replace(/\t/g, " ").replace(/\r$/, ""));
|
|
|
|
const push_candidate = (list, lines, start, end, min_lines, max_lines) => {
|
|
if (start < 0 || end <= start || end >= lines.length) return;
|
|
while (start <= end && !lines[start].trim()) start += 1;
|
|
while (end >= start && !lines[end].trim()) end -= 1;
|
|
const count = end - start + 1;
|
|
if (count < min_lines || count > max_lines) return;
|
|
const snippet = lines.slice(start, end + 1).join("\n").trim();
|
|
if (!snippet) return;
|
|
list.push(snippet);
|
|
};
|
|
|
|
const extract_brace_blocks = (lines, min_lines, max_lines) => {
|
|
const out = [];
|
|
for (let i = 0; i < lines.length; i += 1) {
|
|
if (!lines[i].includes("{")) continue;
|
|
let depth = 0;
|
|
let started = false;
|
|
for (let j = i; j < lines.length; j += 1) {
|
|
const opens = (lines[j].match(/\{/g) || []).length;
|
|
const closes = (lines[j].match(/\}/g) || []).length;
|
|
if (opens > 0) started = true;
|
|
depth += opens;
|
|
depth -= closes;
|
|
if (started && depth === 0 && j > i) {
|
|
push_candidate(out, lines, i, j, min_lines, max_lines);
|
|
break;
|
|
}
|
|
if (depth < 0) break;
|
|
}
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const extract_python_blocks = (lines, min_lines, max_lines) => {
|
|
const out = [];
|
|
const header = /^\s*(def|class|if|elif|else|for|while|try|except|finally|with|match|case)\b.*:\s*$/;
|
|
for (let i = 0; i < lines.length; i += 1) {
|
|
if (!header.test(lines[i])) continue;
|
|
const base = count_indent(lines[i]);
|
|
let started_body = false;
|
|
let end = i;
|
|
for (let j = i + 1; j < lines.length; j += 1) {
|
|
const line = lines[j];
|
|
if (!line.trim()) {
|
|
if (started_body) end = j;
|
|
continue;
|
|
}
|
|
const indent = count_indent(line);
|
|
if (indent > base) {
|
|
started_body = true;
|
|
end = j;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (started_body) push_candidate(out, lines, i, end, min_lines, max_lines);
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const extract_paragraph_chunks = (lines, min_lines, max_lines) => {
|
|
const out = [];
|
|
let start = -1;
|
|
for (let i = 0; i <= lines.length; i += 1) {
|
|
const is_blank = i === lines.length || !lines[i].trim();
|
|
if (!is_blank && start === -1) start = i;
|
|
if (is_blank && start !== -1) {
|
|
push_candidate(out, lines, start, i - 1, min_lines, max_lines);
|
|
start = -1;
|
|
}
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const dedupe_candidates = (items) => Array.from(new Set(items));
|
|
|
|
const snippet_from_text = (text, source = "") => {
|
|
const lines = normalize_lines(text);
|
|
const min_lines = 6;
|
|
const max_lines = 36;
|
|
const ext = extension_from_source(source);
|
|
|
|
let candidates = [];
|
|
|
|
if (["js", "mjs", "cjs", "ts", "tsx", "jsx", "css", "json"].includes(ext)) {
|
|
candidates = candidates.concat(extract_brace_blocks(lines, min_lines, max_lines));
|
|
}
|
|
|
|
if (ext === "py") {
|
|
candidates = candidates.concat(extract_python_blocks(lines, min_lines, max_lines));
|
|
}
|
|
|
|
candidates = dedupe_candidates(candidates);
|
|
if (!candidates.length) {
|
|
candidates = dedupe_candidates(extract_paragraph_chunks(lines, min_lines, max_lines));
|
|
}
|
|
|
|
if (!candidates.length) return "";
|
|
return pick(candidates);
|
|
};
|
|
|
|
const send_json = (res, status, payload) => {
|
|
res.writeHead(status, {
|
|
"Content-Type": "application/json; charset=UTF-8",
|
|
"Cache-Control": "no-store, no-cache, must-revalidate",
|
|
Pragma: "no-cache",
|
|
Expires: "0"
|
|
});
|
|
res.end(JSON.stringify(payload));
|
|
};
|
|
|
|
const send_github_snippet = async (res, track) => {
|
|
const selected_track = sanitize_track(track);
|
|
const source_pool = github_sources_by_track[selected_track] || all_tracks;
|
|
|
|
for (let i = 0; i < 8; i += 1) {
|
|
const source = pick(source_pool);
|
|
const file = pick(source.files);
|
|
const url = `https://raw.githubusercontent.com/${source.repo}/${source.ref}/${file}`;
|
|
|
|
try {
|
|
const text = await fetch_text(url);
|
|
const snippet = snippet_from_text(text, file);
|
|
if (!snippet) continue;
|
|
send_json(res, 200, { snippet, source: `${source.repo}/${file}`, track: selected_track });
|
|
return;
|
|
} catch {
|
|
// Try another repo/file.
|
|
}
|
|
}
|
|
|
|
send_json(res, 502, { error: "could not fetch snippet" });
|
|
};
|
|
|
|
const safe_path = (url_path) => {
|
|
if (!url_path || url_path === "/") return join(public_dir, "index.html");
|
|
const cleaned = normalize(url_path).replace(/^(\.\.([/\\]|$))+/, "");
|
|
return join(public_dir, cleaned);
|
|
};
|
|
|
|
const send = (file_path, res) => {
|
|
readFile(file_path, (err, data) => {
|
|
if (err?.code === "ENOENT") {
|
|
res.writeHead(404, { "Content-Type": "text/plain; charset=UTF-8" });
|
|
res.end("404 not found");
|
|
return;
|
|
}
|
|
if (err) {
|
|
res.writeHead(500, { "Content-Type": "text/plain; charset=UTF-8" });
|
|
res.end("500 internal server error");
|
|
return;
|
|
}
|
|
const type = mime_types[extname(file_path).toLowerCase()] || "application/octet-stream";
|
|
res.writeHead(200, { "Content-Type": type });
|
|
res.end(data);
|
|
});
|
|
};
|
|
|
|
http
|
|
.createServer((req, res) => {
|
|
const request_url = new URL(req.url || "/", "http://localhost");
|
|
const requested_track = request_url.searchParams.get("d") || request_url.searchParams.get("track") || "full-stack";
|
|
const path_only = (req.url || "/").split("?")[0];
|
|
|
|
if (path_only === "/api/snippet") {
|
|
send_github_snippet(res, requested_track);
|
|
return;
|
|
}
|
|
|
|
const file_path = safe_path(path_only);
|
|
stat(file_path, (err, info) => send(!err && info.isDirectory() ? join(file_path, "index.html") : file_path, res));
|
|
})
|
|
.listen(port, () => {
|
|
console.log(`web running at http://localhost:${port}`);
|
|
});
|