Files
Tarun Kumar 1dcda91a36 fix(hardening): fix markdown entity escaping, enrichment starvation, and frontmatter coercion
- Markdown: remove escapeHtml in parseInline to prevent React double-escaping entities in JSX
- Pipeline: prioritize unattempted games over noStore retries in import-games.mjs
- Frontmatter: only coerce known numeric fields (score) to prevent numeric titles/versions from failing string validation
- Workflows: pass step outputs through env in compat-convert.yml to prevent bash script injection
- UI & State: avoid in-place sorting mutation in GamePage and calculate remaining contributors dynamically
- Performance: deduplicate concurrent in-flight fetchContributorCount requests with in-memory caching
- Cleanup: delete unused findGame, timeAgo, and markdownToText dead code
- Security: bump nanoid via npm audit fix
2026-08-18 06:41:12 +05:30

65 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Import the PS5 game title database from andshrew/PlayStation-Titles and
* enrich entries with PlayStation Store metadata (covers, publisher, release
* date, genres).
*
* npm run import # refresh title list (no enrichment)
* npm run import -- --enrich 300 # + enrich 300 concepts with metadata
* npm run import -- --only PPSA01284 # enrich specific title IDs
* npm run import -- --force # accept a >5% shrink of the list
*
* Output: src/data/games.json (committed; regenerated weekly by CI).
*/
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dedupeByConcept, mergeExisting } from "./lib/transform.mjs";
import { applyEnrichment, fetchConcept, guardShrink } from "./lib/enrich.mjs";
const SOURCE = "https://raw.githubusercontent.com/andshrew/PlayStation-Titles/main/Json/PS5_Titles.json";
const OUT = new URL("../src/data/games.json", import.meta.url);
const THROTTLE_MS = 600;
const args = process.argv.slice(2);
const force = args.includes("--force");
const enrichIdx = args.indexOf("--enrich");
const enrichCount = enrichIdx === -1 ? 0 : Number(args[enrichIdx + 1] ?? 0);
const onlyIdx = args.indexOf("--only");
const only = onlyIdx === -1 ? null : (args[onlyIdx + 1] ?? "").split(",").filter(Boolean);
const res = await fetch(SOURCE);
if (!res.ok) throw new Error(`source fetch failed: HTTP ${res.status}`);
const rows = await res.json();
console.log(`source rows: ${rows.length}`);
const fresh = dedupeByConcept(rows);
const existing = JSON.parse(await readFile(OUT, "utf8").catch(() => "[]"));
guardShrink(fresh.length, existing.length, force);
const games = mergeExisting(fresh, existing);
const unattempted = games.filter((g) => !g.enriched && !g.noStore);
const retries = games.filter((g) => !g.enriched && g.noStore);
const pending = only
? games.filter((g) => !g.enriched && g.allTitleIds.some((t) => only.includes(t)))
: [...unattempted, ...retries];
const todo = pending.slice(0, enrichCount);
let done = 0;
for (const game of todo) {
try {
const concept = await fetchConcept(game.conceptId);
Object.assign(game, applyEnrichment(game, concept));
done++;
} catch (err) {
console.error(`enrich ${game.titleId} (${game.conceptId}) failed: ${err.message}`);
if (String(err).includes("hash rejected")) break;
}
await new Promise((r) => setTimeout(r, THROTTLE_MS));
if (done % 25 === 0 && done > 0) console.log(`enriched ${done}/${todo.length}`);
}
await mkdir(new URL("../src/data/", import.meta.url), { recursive: true });
await writeFile(OUT, JSON.stringify(games, null, 1) + "\n");
const enrichedTotal = games.filter((g) => g.enriched).length;
console.log(
`wrote ${games.length} games (${enrichedTotal} enriched, ${games.length - enrichedTotal} pending) to src/data/games.json`,
);