import { readdir, readFile } from 'node:fs/promises'; import path from 'node:path'; // Content packs are discovered from the filesystem, so adding a theme or a // language is just dropping files in a folder: no code change, no rebuild list. const THEME_ID = /^[a-z0-9][a-z0-9-]*$/; const LOCALE_CODE = /^[a-z]{2,3}(-[A-Za-z0-9]+)*$/; async function readJson(file) { return JSON.parse(await readFile(file, 'utf8')); } async function listLocaleCodes(dir) { let names; try { names = await readdir(dir); } catch { return []; } return names .filter((n) => n.endsWith('.json')) .map((n) => n.slice(0, -'.json'.length)) .filter((code) => LOCALE_CODE.test(code)) .sort(); } /** GET /api/themes -> [{ id, order, assets, locales }] */ async function listThemes(publicDir) { const root = path.join(publicDir, 'themes'); const entries = await readdir(root, { withFileTypes: true }).catch(() => []); const themes = []; for (const entry of entries) { if (!entry.isDirectory() || !THEME_ID.test(entry.name)) continue; const dir = path.join(root, entry.name); try { const manifest = await readJson(path.join(dir, 'theme.json')); themes.push({ id: entry.name, order: Number.isFinite(manifest.order) ? manifest.order : 100, assets: manifest.assets && typeof manifest.assets === 'object' ? manifest.assets : {}, locales: await listLocaleCodes(path.join(dir, 'locales')), }); } catch { // Skip folders without a valid theme.json rather than failing the whole list. } } return themes.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id)); } /** GET /api/locales -> [{ code, name }] */ async function listLocales(publicDir) { const dir = path.join(publicDir, 'locales'); const out = []; for (const code of await listLocaleCodes(dir)) { try { const file = await readJson(path.join(dir, `${code}.json`)); out.push({ code, name: file?.meta?.name ?? code }); } catch { // Ignore malformed locale files. } } return out; } /** Returns a JSON payload for known /api routes, or undefined for unknown ones. */ export async function handleApi(publicDir, pathname) { switch (pathname) { case '/api/themes': return listThemes(publicDir); case '/api/locales': return listLocales(publicDir); default: return undefined; } }