fix(rspace): serve canvas through renderShell like all other rapps

The rspace module was serving raw canvas.html as a standalone page
(bypassing renderShell) because canvas-module.html didn't exist.
This meant navigating to rspace caused a full page replacement with
no shared shell, tabs, or TabCache support — appearing to open in a
"new window."

Now extracts body content, styles, and scripts from canvas.html at
startup (stripping the duplicate shell chrome) and renders through
renderShell like every other module. This makes rspace fully
interoperable with the tab system and TabCache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-01 14:55:36 -08:00
parent a39cf6e1c2
commit 6bd23a6778
1 changed files with 74 additions and 18 deletions

View File

@ -16,34 +16,90 @@ const DIST_DIR = resolve(import.meta.dir, "../../dist");
const routes = new Hono();
/**
* Extract body content and scripts from the full canvas.html page.
* Strips the shell chrome (header, tab-bar, welcome overlay) that renderShell provides,
* and returns just the canvas-specific DOM + inline styles + module scripts.
*/
function extractCanvasContent(html: string): { body: string; styles: string; scripts: string } {
// Extract inline <style> blocks from <head> (canvas CSS)
const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
const headContent = headMatch?.[1] || "";
const styleBlocks: string[] = [];
headContent.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (match) => {
styleBlocks.push(match);
return "";
});
// Extract module script src from <head>
const scriptSrcs: string[] = [];
headContent.replace(/<script[^>]*\bsrc="([^"]+)"[^>]*>/gi, (_m, src: string) => {
if (!src.includes("shell.js")) scriptSrcs.push(src);
return "";
});
// Extract <body> content
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
let bodyContent = bodyMatch?.[1] || "";
// Strip shell chrome that renderShell already provides:
// header, tab-bar (contains only a <rstack-tab-bar>), and welcome overlay.
bodyContent = bodyContent
.replace(/<header[\s\S]*?<\/header>/i, "")
.replace(/<div class="rstack-tab-row"[^>]*>\s*<rstack-tab-bar[^>]*><\/rstack-tab-bar>\s*<\/div>/i, "")
.replace(/<!--\s*Welcome overlay[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/i, "");
const styles = styleBlocks.join("\n");
const scripts = scriptSrcs.map(s => `<script type="module" crossorigin src="${s}"></script>`).join("\n");
return { body: bodyContent.trim(), styles, scripts };
}
// Cache the extracted canvas content (parsed once at startup)
let canvasContentCache: { body: string; styles: string; scripts: string } | null = null;
async function getCanvasContent(): Promise<{ body: string; styles: string; scripts: string }> {
if (canvasContentCache) return canvasContentCache;
// Prefer canvas-module.html (pre-extracted body partial)
const moduleFile = Bun.file(resolve(DIST_DIR, "canvas-module.html"));
if (await moduleFile.exists()) {
canvasContentCache = {
body: await moduleFile.text(),
styles: "",
scripts: `<script type="module" src="/canvas-module.js"></script>`,
};
return canvasContentCache;
}
// Fall back to extracting from full canvas.html
const fullFile = Bun.file(resolve(DIST_DIR, "canvas.html"));
if (await fullFile.exists()) {
canvasContentCache = extractCanvasContent(await fullFile.text());
return canvasContentCache;
}
return {
body: `<div style="padding:2rem;text-align:center;color:#64748b;">Canvas loading...</div>`,
styles: "",
scripts: "",
};
}
// GET / — serve the canvas page wrapped in shell
routes.get("/", async (c) => {
const spaceSlug = c.req.param("space") || c.req.query("space") || "demo";
// Read the canvas page template from dist
const canvasFile = Bun.file(resolve(DIST_DIR, "canvas-module.html"));
let canvasBody = "";
if (await canvasFile.exists()) {
canvasBody = await canvasFile.text();
} else {
// Fallback: serve full canvas.html directly if module template not built yet
const fallbackFile = Bun.file(resolve(DIST_DIR, "canvas.html"));
if (await fallbackFile.exists()) {
return new Response(fallbackFile, {
headers: { "Content-Type": "text/html" },
});
}
canvasBody = `<div style="padding:2rem;text-align:center;color:#64748b;">Canvas loading...</div>`;
}
const canvas = await getCanvasContent();
const html = renderShell({
title: `${spaceSlug} — Canvas | rSpace`,
moduleId: "rspace",
spaceSlug,
body: canvasBody,
body: canvas.body,
modules: getModuleInfoList(),
theme: "dark",
scripts: `<script type="module" src="/canvas-module.js"></script>`,
styles: canvas.styles,
scripts: canvas.scripts,
});
return c.html(html);