build-index.mjs (2964B)
1 /** 2 * `npm run build:index` — project the committed dataset down to the fields the 3 * catalog list needs and write it as a content-addressed file: 4 * 5 * public/data/index-<sha256[0:8]>.json fetched by the catalog island 6 * src/data/index-hash.json { file, hash, bytes, count } — imported 7 * by catalog.astro to point at the file 8 * 9 * The hashed name lets vercel.json serve it `immutable`; a data change makes a 10 * new name, so a stale cache is impossible. Runs at the head of `npm run build` 11 * and `npm run dev`; validate-data.mjs fails if index-hash.json is stale. 12 */ 13 import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, existsSync } from 'node:fs'; 14 import { createHash } from 'node:crypto'; 15 import { fileURLToPath } from 'node:url'; 16 import { dirname, join, resolve } from 'node:path'; 17 18 const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); 19 20 /** Mirror of LIST_FIELDS in src/lib/render-row.ts (test/build-index.test.mjs pins them). */ 21 export const LIST_FIELDS = [ 22 'id', 'toolId', 'toolName', 'name', 'source', 'platform', 'capability', 'nativeCategory', 23 'command', 'description', 'usecase', 'mitre', 'privilege', 'added', 24 'context', 'requires', 'services', 'environment', 'aliases', 'availability', 'verification', 25 'reviewedAt', 'compatibility', 'expected', 'troubleshooting', 'sideEffects', 'restore', 'template', 'references', 'detection', 26 ]; 27 28 export function project(t) { 29 const out = {}; 30 for (const k of LIST_FIELDS) if (t[k] !== undefined) out[k] = t[k]; 31 return out; 32 } 33 34 /** Deterministic: same techniques → same bytes → same hash. */ 35 export function buildIndex(techniques) { 36 const list = techniques.map(project); 37 const json = JSON.stringify(list); 38 const hash = createHash('sha256').update(json).digest('hex').slice(0, 8); 39 return { json, hash, file: `index-${hash}.json`, bytes: Buffer.byteLength(json), count: list.length }; 40 } 41 42 export function main() { 43 const techniques = JSON.parse(readFileSync(join(ROOT, 'src/data/techniques.json'), 'utf8')); 44 const idx = buildIndex(techniques); 45 const pub = join(ROOT, 'public', 'data'); 46 mkdirSync(pub, { recursive: true }); 47 for (const f of readdirSync(pub)) if (/^index-[0-9a-f]{8}\.json$/.test(f) && f !== idx.file) unlinkSync(join(pub, f)); 48 writeFileSync(join(pub, idx.file), idx.json); 49 const meta = { file: idx.file, hash: idx.hash, bytes: idx.bytes, count: idx.count }; 50 const metaPath = join(ROOT, 'src/data/index-hash.json'); 51 const prev = existsSync(metaPath) ? readFileSync(metaPath, 'utf8') : ''; 52 const next = JSON.stringify(meta, null, 2) + '\n'; 53 if (prev !== next) writeFileSync(metaPath, next); 54 console.log(`build:index — public/data/${idx.file}: ${idx.count} rows, ${idx.bytes.toLocaleString('en-US')} B${prev !== next ? ' (index-hash.json updated)' : ''}`); 55 } 56 57 if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();