77 lines
2.3 KiB
JavaScript
77 lines
2.3 KiB
JavaScript
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;
|
|
}
|
|
}
|