// The area plugins of wayBetter for X on the site (/store/areas//): Compose+, Reader+, … are // built into the app, so they are not in the signed catalog and have no Install button. Their // names, descriptions and feature lists come from the app's own feature table and words // (apps/desktop/src/areas/table.ts, apps/desktop/locales/.json); the folder // store/x/areas//item.json adds category, tags, licence, credits and media // (scripts/store/areas.mjs). So the page, the app and the Store check read one source. import { existsSync, readFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; // @ts-expect-error: a plain ES module shared with scripts/check-store.mjs import { readAreaSource } from '../../scripts/store/areas.mjs'; import { X_TABLE } from '../desktop/src/areas/table.ts'; import { featureWords } from '../../waybetter/packages/runtime/src/featureTable.ts'; const HERE = dirname(fileURLToPath(import.meta.url)); const REPO = resolve(HERE, '../..'); const LOCALES = join(REPO, 'apps/desktop/locales'); /** The app version these pages speak of: the first public release has every area. */ export const AREAS_SINCE = '1.0.0'; export type AreaFeature = { id: string; label: string; description: string }; export type AreaItem = { id: string; name: string; description: string; category: string; tags: string[]; license?: string; credits?: string; features: AreaFeature[]; }; type Area = { id: string; category: string; tags: string[]; license?: string; credits?: string; dir: string }; let source: Area[] | null = null; const words = new Map>(); /** The area folders (store/x/areas/), read once; a problem stops the build as the Store check would. */ function areaSource(): Area[] { if (source) return source; const read = readAreaSource({ root: REPO, app: 'x', ids: X_TABLE.areas.map(area => area.id) }) as { entries: Area[]; problems: string[] }; if (read.problems.length) throw new Error(`the Store's area pages have problems (bun run check:store explains each):\n ${read.problems.join('\n ')}`); source = read.entries; return source; } /** The app's words in a language, English underneath (a site language the app lacks reads English). */ function appWords(lang: string): Record { const cached = words.get(lang); if (cached) return cached; const load = (code: string) => { const file = join(LOCALES, `${code}.json`); return existsSync(file) ? (JSON.parse(readFileSync(file, 'utf8')) as { messages: Record }).messages : {}; }; const out = { ...load('en'), ...(lang === 'en' ? {} : load(lang)) }; words.set(lang, out); return out; } /** Every area in the table's order, in a language (the app's own words for it). */ export function areaItems(lang = 'en'): AreaItem[] { const text = appWords(lang); const say = (key: string, fallback: string) => (text[key] ?? fallback).replace(/\s+/g, ' ').trim(); return areaSource().map(entry => { const area = X_TABLE.areas.find(candidate => candidate.id === entry.id)!; const features = X_TABLE.features .filter(feature => feature.area === area.id) .map(feature => { const keys = featureWords(feature); return { id: feature.id, label: say(keys.label, feature.id), description: say(keys.description, '') }; }); return { id: area.id, name: area.name, description: say(area.description, area.name), category: entry.category, tags: entry.tags, ...(entry.license ? { license: entry.license } : {}), ...(entry.credits ? { credits: entry.credits } : {}), features, }; }); } export const areaPath = (id: string): string => `/store/areas/${id}/`; /** The folder with the area's item.json and media ("store/x/areas/reader"). */ export const areaFolder = (id: string): string => join(REPO, 'store', 'x', 'areas', id);