/**
* 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 } from "../shared/module";
import { getDocumentData } from "./community-store";
// ── 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
${styles}
${head}
${body}
${renderWelcomeOverlay()}
${scripts}