👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

biome check and allow scripts dir

+84 -74
+1
biome.json
··· 9 9 "ignoreUnknown": false, 10 10 "includes": [ 11 11 "**/src/**/*", 12 + "**/scripts/**/*", 12 13 "**/.vscode/**/*", 13 14 "**/index.html", 14 15 "**/vite.config.ts",
+15 -15
scripts/download-scryfall.test.ts
··· 1 - import { describe, it, expect } from "vitest"; 2 - import { isDefaultPrinting, compareCards } from "./download-scryfall.ts"; 1 + import { describe, expect, it } from "vitest"; 3 2 import type { Card } from "../src/lib/scryfall-types.ts"; 4 - import { asScryfallId, asOracleId } from "../src/lib/scryfall-types.ts"; 3 + import { asOracleId, asScryfallId } from "../src/lib/scryfall-types.ts"; 4 + import { compareCards, isDefaultPrinting } from "./download-scryfall.ts"; 5 5 6 6 type TestCard = Card & { security_stamp?: string; [key: string]: unknown }; 7 7 ··· 48 48 expect( 49 49 isDefaultPrinting(createCard({ frame_effects: ["extendedart"] })), 50 50 ).toBe(false); 51 - expect( 52 - isDefaultPrinting(createCard({ frame_effects: ["showcase"] })), 53 - ).toBe(false); 54 - expect( 55 - isDefaultPrinting(createCard({ frame_effects: ["inverted"] })), 56 - ).toBe(false); 51 + expect(isDefaultPrinting(createCard({ frame_effects: ["showcase"] }))).toBe( 52 + false, 53 + ); 54 + expect(isDefaultPrinting(createCard({ frame_effects: ["inverted"] }))).toBe( 55 + false, 56 + ); 57 57 }); 58 58 59 59 it("rejects full art", () => { ··· 73 73 }); 74 74 75 75 it("rejects special promo types (serialized, doublerainbow, etc)", () => { 76 - expect( 77 - isDefaultPrinting(createCard({ promo_types: ["serialized"] })), 78 - ).toBe(false); 76 + expect(isDefaultPrinting(createCard({ promo_types: ["serialized"] }))).toBe( 77 + false, 78 + ); 79 79 expect( 80 80 isDefaultPrinting( 81 81 createCard({ promo_types: ["serialized", "doublerainbow"] }), ··· 87 87 expect( 88 88 isDefaultPrinting(createCard({ promo_types: ["confettifoil"] })), 89 89 ).toBe(false); 90 - expect( 91 - isDefaultPrinting(createCard({ promo_types: ["galaxyfoil"] })), 92 - ).toBe(false); 90 + expect(isDefaultPrinting(createCard({ promo_types: ["galaxyfoil"] }))).toBe( 91 + false, 92 + ); 93 93 expect(isDefaultPrinting(createCard({ promo_types: ["textured"] }))).toBe( 94 94 false, 95 95 );
+68 -59
scripts/download-scryfall.ts
··· 23 23 */ 24 24 25 25 import { createHash } from "node:crypto"; 26 - import { writeFile, mkdir, readFile } from "node:fs/promises"; 27 - import { join, dirname } from "node:path"; 26 + import { mkdir, readFile, writeFile } from "node:fs/promises"; 27 + import { dirname, join } from "node:path"; 28 28 import { fileURLToPath } from "node:url"; 29 29 import type { Card, CardDataOutput } from "../src/lib/scryfall-types.ts"; 30 - import { asScryfallId, asOracleId } from "../src/lib/scryfall-types.ts"; 30 + import { asOracleId, asScryfallId } from "../src/lib/scryfall-types.ts"; 31 31 32 32 const __filename = fileURLToPath(import.meta.url); 33 33 const __dirname = dirname(__filename); ··· 112 112 variation?: boolean; 113 113 [key: string]: unknown; 114 114 } 115 - 116 115 117 116 interface BulkDataItem { 118 117 type: string; ··· 248 247 // Note: finishes is optional, so we're lenient here 249 248 if (card.finishes) { 250 249 const validFinishes = ["nonfoil", "foil"]; 251 - const hasValidFinish = card.finishes.some((f) => 252 - validFinishes.includes(f), 253 - ); 250 + const hasValidFinish = card.finishes.some((f) => validFinishes.includes(f)); 254 251 if (!hasValidFinish) { 255 252 return false; 256 253 } ··· 277 274 if (!aDefault && bDefault) return 1; 278 275 279 276 // Deprioritize Arena-only (paper and MTGO are both fine) 280 - const aArenaOnly = 281 - a.games?.length === 1 && a.games[0] === "arena"; 282 - const bArenaOnly = 283 - b.games?.length === 1 && b.games[0] === "arena"; 277 + const aArenaOnly = a.games?.length === 1 && a.games[0] === "arena"; 278 + const bArenaOnly = b.games?.length === 1 && b.games[0] === "arena"; 284 279 if (!aArenaOnly && bArenaOnly) return -1; 285 280 if (aArenaOnly && !bArenaOnly) return 1; 286 281 ··· 309 304 if (!frame) return 100; 310 305 if (frame === "future") return 99; // quirky futuresight aesthetic 311 306 const year = parseInt(frame, 10); 312 - if (!isNaN(year)) return -year; // newer years = lower rank = preferred 307 + if (!Number.isNaN(year)) return -year; // newer years = lower rank = preferred 313 308 return -10000; // unknown non-numeric frame, assume it's new and prefer it 314 309 }; 315 310 const aFrameRank = getFrameRank(a.frame); ··· 376 371 async function getCachedSymbolNames(): Promise<string[]> { 377 372 const symbolsCachePath = join(TEMP_DIR, "symbols-cache.json"); 378 373 try { 379 - const cached = JSON.parse(await readFile(symbolsCachePath, "utf-8")) as string[]; 374 + const cached = JSON.parse( 375 + await readFile(symbolsCachePath, "utf-8"), 376 + ) as string[]; 380 377 // Cache stores full symbols like "{W}", extract names 381 378 return cached.map((s) => s.replace(/[{}]/g, "")).sort(); 382 379 } catch { 383 - console.warn("Warning: No cached symbols found, VALID_SYMBOLS will be empty"); 380 + console.warn( 381 + "Warning: No cached symbols found, VALID_SYMBOLS will be empty", 382 + ); 384 383 return []; 385 384 } 386 385 } ··· 447 446 const rawData: ScryfallCard[] = JSON.parse(await readFile(tempFile, "utf-8")); 448 447 449 448 // Build raw card map for sorting (before filtering strips fields like security_stamp) 450 - const rawCardById = Object.fromEntries(rawData.map((card) => [card.id, card])); 449 + const rawCardById = Object.fromEntries( 450 + rawData.map((card) => [card.id, card]), 451 + ); 451 452 452 453 const cards = rawData.map(filterCard); 453 454 console.log(`Filtered ${cards.length} cards`); ··· 456 457 console.log("Building indexes..."); 457 458 const cardById = Object.fromEntries(cards.map((card) => [card.id, card])); 458 459 459 - const oracleIdToPrintings = cards.reduce<CardDataOutput["oracleIdToPrintings"]>( 460 - (acc, card) => { 461 - if (!acc[card.oracle_id]) { 462 - acc[card.oracle_id] = []; 463 - } 464 - acc[card.oracle_id].push(card.id); 465 - return acc; 466 - }, 467 - {}, 468 - ); 460 + const oracleIdToPrintings = cards.reduce< 461 + CardDataOutput["oracleIdToPrintings"] 462 + >((acc, card) => { 463 + if (!acc[card.oracle_id]) { 464 + acc[card.oracle_id] = []; 465 + } 466 + acc[card.oracle_id].push(card.id); 467 + return acc; 468 + }, {}); 469 469 470 470 // Sort printings by canonical order (most canonical first) 471 471 // Uses raw cards for comparison (has fields like security_stamp that get stripped) ··· 541 541 ); 542 542 } 543 543 544 - const chunkFilenames = await Promise.all(Array.from({length: chunkCount}, async (_, chunkIndex) => { 545 - const start = chunkIndex * CARDS_PER_CHUNK; 546 - const end = Math.min(start + CARDS_PER_CHUNK, cardEntries.length); 547 - const chunkEntries = cardEntries.slice(start, end); 544 + const chunkFilenames = await Promise.all( 545 + Array.from({ length: chunkCount }, async (_, chunkIndex) => { 546 + const start = chunkIndex * CARDS_PER_CHUNK; 547 + const end = Math.min(start + CARDS_PER_CHUNK, cardEntries.length); 548 + const chunkEntries = cardEntries.slice(start, end); 549 + 550 + const chunkData = { cards: Object.fromEntries(chunkEntries) }; 551 + const chunkContent = JSON.stringify(chunkData); 552 + const contentHash = createHash("sha256") 553 + .update(chunkContent) 554 + .digest("hex") 555 + .slice(0, 16); 556 + const chunkFilename = `cards-${String(chunkIndex).padStart(3, "0")}-${contentHash}.json`; 548 557 549 - const chunkData = { cards: Object.fromEntries(chunkEntries) }; 550 - const chunkContent = JSON.stringify(chunkData); 551 - const contentHash = createHash("sha256") 552 - .update(chunkContent) 553 - .digest("hex") 554 - .slice(0, 16); 555 - const chunkFilename = `cards-${String(chunkIndex).padStart(3, "0")}-${contentHash}.json`; 556 - 557 - await writeFile(join(CARDS_DIR, chunkFilename), chunkContent) 558 - console.log( 559 - `Wrote ${chunkFilename}: ${chunkEntries.length} cards, ${(chunkContent.length / 1024 / 1024).toFixed(2)}MB`, 560 - ); 558 + await writeFile(join(CARDS_DIR, chunkFilename), chunkContent); 559 + console.log( 560 + `Wrote ${chunkFilename}: ${chunkEntries.length} cards, ${(chunkContent.length / 1024 / 1024).toFixed(2)}MB`, 561 + ); 561 562 562 - return chunkFilename 563 - })) 563 + return chunkFilename; 564 + }), 565 + ); 564 566 565 567 // Write indexes file with content hash (oracle mappings for client) 566 568 // oracleIdToPrintings is sorted by canonical order - first element is the canonical printing ··· 581 583 582 584 // Write version.json at top level for quick version checks 583 585 const versionData = { version: data.version, cardCount: data.cardCount }; 584 - await writeFile(join(OUTPUT_DIR, "version.json"), JSON.stringify(versionData)); 586 + await writeFile( 587 + join(OUTPUT_DIR, "version.json"), 588 + JSON.stringify(versionData), 589 + ); 585 590 console.log(`Wrote version.json`); 586 591 587 592 console.log(`\nTotal chunks: ${chunkFilenames.length}`); ··· 799 804 const parsePriceToCents = (price: string | null | undefined): number => { 800 805 if (price == null) return NULL_VALUE; 801 806 const parsed = parseFloat(price); 802 - if (isNaN(parsed)) return NULL_VALUE; 807 + if (Number.isNaN(parsed)) return NULL_VALUE; 803 808 return Math.round(parsed * 100); 804 809 }; 805 810 ··· 944 949 945 950 await Promise.all( 946 951 symbology.data.map((symbol) => { 947 - const filename = symbol.symbol.replace(/[{}\/]/g, "").toLowerCase(); 952 + const filename = symbol.symbol.replace(/[{}/]/g, "").toLowerCase(); 948 953 const outputPath = join(SYMBOLS_DIR, `${filename}.svg`); 949 954 return downloadFile(symbol.svg_uri, outputPath); 950 955 }), ··· 953 958 // Cache the symbol list 954 959 await writeFile(symbolsCachePath, JSON.stringify(currentSymbols)); 955 960 956 - console.log(`Downloaded ${symbology.data.length} symbol SVGs to: ${SYMBOLS_DIR}`); 961 + console.log( 962 + `Downloaded ${symbology.data.length} symbol SVGs to: ${SYMBOLS_DIR}`, 963 + ); 957 964 return { count: symbology.data.length, names: symbolNames }; 958 965 } 959 966 ··· 989 996 // Check if we already have this version 990 997 let needsDownload = true; 991 998 try { 992 - const cached = JSON.parse( 993 - await readFile(versionCachePath, "utf-8"), 994 - ) as { version: string }; 999 + const cached = JSON.parse(await readFile(versionCachePath, "utf-8")) as { 1000 + version: string; 1001 + }; 995 1002 996 1003 if (cached.version === version) { 997 1004 console.log(`✓ Already have Keyrune ${version}, skipping download`); ··· 1017 1024 console.log(`Fetching: ${keyruneBase}/css/keyrune.css`); 1018 1025 const cssResponse = await fetch(`${keyruneBase}/css/keyrune.css`); 1019 1026 if (!cssResponse.ok) { 1020 - throw new Error(`HTTP ${cssResponse.status}: ${cssResponse.statusText}`); 1027 + throw new Error( 1028 + `HTTP ${cssResponse.status}: ${cssResponse.statusText}`, 1029 + ); 1021 1030 } 1022 1031 const css = await cssResponse.text(); 1023 1032 await writeFile(cssCachePath, css); ··· 1028 1037 } else { 1029 1038 // Offline mode: read cached version 1030 1039 try { 1031 - const cached = JSON.parse( 1032 - await readFile(versionCachePath, "utf-8"), 1033 - ) as { version: string }; 1040 + const cached = JSON.parse(await readFile(versionCachePath, "utf-8")) as { 1041 + version: string; 1042 + }; 1034 1043 version = cached.version; 1035 1044 console.log(`Using cached Keyrune ${version}`); 1036 1045 } catch { ··· 1052 1061 const blockRegex = 1053 1062 /((?:\.ss-[a-z0-9]+:before[,\s]*)+)\s*\{\s*content:\s*"\\([0-9a-f]+)"/gi; 1054 1063 const selectorRegex = /\.ss-([a-z0-9]+):before/g; 1055 - let match: RegExpExecArray | null; 1056 1064 1057 - while ((match = blockRegex.exec(css)) !== null) { 1065 + for (const match of css.matchAll(blockRegex)) { 1058 1066 const [, selectors, codepoint] = match; 1059 1067 const cp = parseInt(codepoint, 16); 1060 1068 1061 1069 // Extract all set codes from the selector list 1062 - let selectorMatch: RegExpExecArray | null; 1063 - while ((selectorMatch = selectorRegex.exec(selectors)) !== null) { 1070 + for (const selectorMatch of selectors.matchAll(selectorRegex)) { 1064 1071 mappings[selectorMatch[1]] = cp; 1065 1072 } 1066 1073 } ··· 1145 1152 1146 1153 console.log("\n=== Summary ==="); 1147 1154 console.log(`Cards: ${cards.data.cardCount.toLocaleString()}`); 1148 - console.log(`Migrations: ${Object.keys(migrations).length.toLocaleString()}`); 1155 + console.log( 1156 + `Migrations: ${Object.keys(migrations).length.toLocaleString()}`, 1157 + ); 1149 1158 console.log(`Mana symbols: ${symbols.count.toLocaleString()}`); 1150 1159 console.log(`Set symbols: ${keyrune.setCount.toLocaleString()}`); 1151 1160 console.log(`Version: ${cards.data.version}`);