Adds the foundation of the feature-module architecture (docs/frontend-modularization.md) as inert, additive code: - kernel/feature-registry.js: window.LP.features — runtime register(), manifest loader, route-table + nav builders. - features/manifest.dev.json: hand-committed manifest mirroring spa.js setupRoutes() exactly (route -> handler + navId). - index.html loads the kernel before spa.js. Zero behaviour change: nothing consults the registry yet. Phase 0b flips routing to be registry-driven with the spa.js Map as fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
94 lines
3.6 KiB
JavaScript
94 lines
3.6 KiB
JavaScript
// kernel/feature-registry.js — the single source of truth for "what pages exist".
|
|
//
|
|
// This is the first piece of the feature-module architecture (see
|
|
// docs/frontend-modularization.md). It replaces the four hand-maintained
|
|
// registries the SPA grew over time:
|
|
// 1. the eager <script> list in index.html,
|
|
// 2. spa.js's hardcoded `routes` Map (setupRoutes),
|
|
// 3. system-loader.js's component registry,
|
|
// 4. config-manager.js's renderConfig() category if-chain.
|
|
//
|
|
// In this first phase it is deliberately *passive*: it loads a checked-in
|
|
// manifest describing today's pages and exposes a routes Map. spa.js consults
|
|
// it for routing; rendering still happens in the existing handleX() bodies.
|
|
// Later phases replace handlers with self-registering feature modules that call
|
|
// LP.features.register({...}) — the runtime registry below already supports it.
|
|
(function () {
|
|
const LP = (window.LP = window.LP || {});
|
|
|
|
LP.features = {
|
|
// Runtime registrations (feature index.js files call register()). Unused in
|
|
// phase 0 — present so later phases can land without touching this file.
|
|
_modules: new Map(),
|
|
|
|
// The parsed manifest (the generated/checked-in description of all pages).
|
|
_manifest: null,
|
|
|
|
// Register a live feature module. id must be unique; last-wins with a warn
|
|
// so a duplicate during migration is loud, not silent.
|
|
register(def) {
|
|
if (!def || !def.id) {
|
|
console.error('[features] register() called without an id', def);
|
|
return;
|
|
}
|
|
if (this._modules.has(def.id)) {
|
|
console.warn(`[features] duplicate registration for "${def.id}" — last wins`);
|
|
}
|
|
this._modules.set(def.id, def);
|
|
return def;
|
|
},
|
|
|
|
get(id) {
|
|
return this._modules.get(id) || null;
|
|
},
|
|
|
|
// Fetch + cache the manifest. Read-only GET of a static/generated file,
|
|
// same pattern as DataLoader.loadApps(). Returns the manifest or null on
|
|
// failure (callers fall back to their built-in defaults).
|
|
async loadManifest(url = '/features/manifest.dev.json') {
|
|
if (this._manifest) return this._manifest;
|
|
try {
|
|
const res = await fetch(url, { cache: 'no-cache' });
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const data = await res.json();
|
|
this._manifest = Array.isArray(data.features) ? data : { version: 0, features: [] };
|
|
return this._manifest;
|
|
} catch (err) {
|
|
console.warn('[features] manifest load failed, callers will fall back:', err.message);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
// The manifest entries, or [] if not loaded.
|
|
list() {
|
|
return (this._manifest && this._manifest.features) || [];
|
|
},
|
|
|
|
// Build a route-pattern -> entry Map from the manifest. The pattern strings
|
|
// (including the trailing-'*' wildcards) are kept verbatim so the consumer's
|
|
// matching logic is unchanged from spa.js's findRouteHandler.
|
|
buildRouteTable() {
|
|
const table = new Map();
|
|
for (const f of this.list()) {
|
|
for (const route of (f.routes || [])) {
|
|
if (table.has(route)) {
|
|
console.warn(`[features] duplicate route "${route}" (feature "${f.id}") — last wins`);
|
|
}
|
|
table.set(route, f);
|
|
}
|
|
}
|
|
return table;
|
|
},
|
|
|
|
// Nav entries (topbar) derived from the manifest, sorted by nav.order.
|
|
// Phase 0 keeps topbar.html authoritative; this is here for the nav kernel
|
|
// that lands with the topbar migration.
|
|
navItems() {
|
|
return this.list()
|
|
.filter(f => f.nav && f.nav.label)
|
|
.sort((a, b) => (a.nav.order || 0) - (b.nav.order || 0))
|
|
.map(f => ({ id: f.id, ...f.nav, routes: f.routes }));
|
|
},
|
|
};
|
|
})();
|