Files
Claude 3adebb731f Rename FunPack to FunUI (5.0.3 Classy Chopper): /fun endpoint, wink logo, load-time migration of old names
Old project/workflow names and browser settings migrate on load; /funpack redirects to /fun.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016acRss69eH1235eRdFTZHY
2026-10-25 13:11:03 +00:00

40 lines
2.4 KiB
JavaScript

// Loads UI features. A feature file's default export is one hub, or several:
// { id, mount, needs?, setup({ host, app }) -> teardown? }
// `mount` names a region (see mounts.js). A feature that cannot load, names a region nobody offers,
// needs a service the app lacks, names (`modules`) a backend module the server does not have, or throws in setup is hidden -- nothing of it reaches the screen.
import { hostFor, offered } from "./mounts.js";
function mountOne(hub, has, app) {
const host = hostFor(hub.mount);
if (!host) throw new Error(`no region offers "${hub.mount}" (offered: ${offered().join(", ")})`);
const lacking = (hub.needs || []).filter((name) => !has(name));
if (lacking.length) throw new Error(`needs ${lacking.join(", ")}`);
const gone = (hub.modules || []).filter((id) => app.hasModule && !app.hasModule(id)); // a button whose backend did not load would only fail when pressed
if (gone.length) throw new Error(`module ${gone.join(", ")} is not installed`);
const before = [...host.childNodes];
const heard = []; // what the feature subscribed to, so a failed one can be unsubscribed
const mine = { ...app, on: (fn) => { const off = app.on(fn); heard.push(off); return off; } };
try {
return { id: hub.id, teardown: hub.setup({ host, app: mine }) };
} catch (err) { // whatever it put on the page and listened to goes with it
[...host.childNodes].filter((n) => !before.includes(n)).forEach((n) => n.remove());
heard.forEach((off) => off());
throw err;
}
}
// `app` is what the core offers features (project, ...). A feature names what it needs; a missing one hides it.
export async function loadFeatures(paths, { load = (path) => import(path), app = {}, has = (name) => name in app, onProgress = () => {} } = {}) {
const mounted = [], hidden = [];
const hide = (id, err) => { hidden.push({ id, why: err.message }); console.warn(`[FunUI] ${id} hidden: ${err.message}`); };
for (const [i, path] of paths.entries()) {
try { onProgress({ n: i + 1, of: paths.length, path }); } catch (err) { console.warn(err); } // a broken progress display must not stop loading
let hubs;
try { hubs = [].concat((await load(path)).default); } catch (err) { hide(path, err); continue; }
for (const hub of hubs) {
try { mounted.push(mountOne(hub, has, app)); } catch (err) { hide(hub?.id || path, err); }
}
}
return { mounted, hidden };
}