/**
* Shell HTML renderer.
*
* Wraps module content in the shared rSpace layout: header with app/space
* switchers + identity, with module content, shell script + styles.
*/
import { resolve } from "node:path";
import type { ModuleInfo, SubPageInfo, OnboardingAction } from "../shared/module";
import { getDocumentData } from "./community-store";
// ── Browser compatibility polyfills (inline, runs before ES modules) ──
const COMPAT_POLYFILLS = ``;
// ── Dynamic per-module favicon (inline, runs after body parse) ──
// Badge map mirrors MODULE_BADGES from rstack-app-switcher.ts — kept in sync manually.
const FAVICON_BADGE_MAP: Record = {
rspace: { badge: "r🎨", color: "#5eead4" },
rnotes: { badge: "r📝", color: "#fcd34d" },
rpubs: { badge: "r📖", color: "#fda4af" },
rswag: { badge: "r👕", color: "#fda4af" },
rsplat: { badge: "r🔮", color: "#d8b4fe" },
rcal: { badge: "r📅", color: "#7dd3fc" },
rtrips: { badge: "r✈️", color: "#6ee7b7" },
rmaps: { badge: "r🗺", color: "#86efac" },
rchats: { badge: "r🗨", color: "#6ee7b7" },
rinbox: { badge: "r📨", color: "#a5b4fc" },
rmail: { badge: "r✉️", color: "#93c5fd" },
rforum: { badge: "r💬", color: "#fcd34d" },
rmeets: { badge: "r📹", color: "#67e8f9" },
rchoices: { badge: "r☑️", color: "#f0abfc" },
rvote: { badge: "r🗳", color: "#c4b5fd" },
rflows: { badge: "r🌊", color: "#bef264" },
rwallet: { badge: "r💰", color: "#fde047" },
rcart: { badge: "r🛒", color: "#fdba74" },
rauctions: { badge: "r🏛", color: "#fca5a5" },
rtube: { badge: "r🎬", color: "#f9a8d4" },
rphotos: { badge: "r📸", color: "#f9a8d4" },
rnetwork: { badge: "r🌐", color: "#93c5fd" },
rsocials: { badge: "r📢", color: "#7dd3fc" },
rfiles: { badge: "r📁", color: "#67e8f9" },
rbooks: { badge: "r📚", color: "#fda4af" },
rdata: { badge: "r📊", color: "#d8b4fe" },
rbnb: { badge: "r🏠", color: "#fbbf24" },
rvnb: { badge: "r🚐", color: "#a5f3fc" },
rtasks: { badge: "r📋", color: "#cbd5e1" },
rschedule: { badge: "r⏱", color: "#a5b4fc" },
rids: { badge: "r🪪", color: "#6ee7b7" },
rstack: { badge: "r✨", color: "#c4b5fd" },
};
const FAVICON_BADGE_JSON = JSON.stringify(FAVICON_BADGE_MAP);
/** Generate an inline script that sets the favicon to the module's badge SVG */
function faviconScript(moduleId: string): string {
return ``;
}
// ── Content-hash cache busting ──
let moduleHashes: Record = {};
try {
const hashFile = resolve(import.meta.dir, "../dist/module-hashes.json");
moduleHashes = JSON.parse(await Bun.file(hashFile).text());
} catch { /* dev mode or first run — no hashes yet */ }
/** Append ?v= to module/shell asset URLs in rendered HTML. */
export function versionAssetUrls(html: string): string {
return html.replace(
/(["'])(\/(?:modules\/[^"'?]+\.(?:js|css)|shell\.js|shell\.css|theme\.css))(?:\?v=[^"']*)?(\1)/g,
(_, q, path, qEnd) => {
const hash = moduleHashes[path];
return hash ? `${q}${path}?v=${hash}${qEnd}` : `${q}${path}${qEnd}`;
}
);
}
/** Extract enabledModules and encryption status from a loaded space. */
export function getSpaceShellMeta(spaceSlug: string): { enabledModules: string[] | null; spaceEncrypted: boolean } {
const data = getDocumentData(spaceSlug);
return {
enabledModules: data?.meta?.enabledModules ?? null,
spaceEncrypted: !!data?.meta?.encrypted,
};
}
export interface ShellOptions {
/** Page */
title: string;
/** Current module ID (highlighted in app switcher) */
moduleId: string;
/** Current space slug */
spaceSlug: string;
/** Space display name */
spaceName?: string;
/** Module HTML content to inject into */
body: string;
/** Additional
${COMPAT_POLYFILLS}
${styles}
${head}
${renderModuleSubNav(moduleId, spaceSlug, visibleModules, opts.isSubdomain ?? IS_PRODUCTION)}
${opts.tabs ? renderTabBar(opts.tabs, opts.activeTab, opts.tabBasePath || ((opts.isSubdomain ?? IS_PRODUCTION) ? `/${escapeAttr(moduleId)}` : `/${escapeAttr(spaceSlug)}/${escapeAttr(moduleId)}`)) : ''}
${body}
${renderWelcomeOverlay()}
${scripts}