// lib/page.js // The Builder SPA shell page (ARCHITECTURE.md 8.2). // // renderEditorShell(req, res) builds a COMPLETE, dependency-free HTML document // (no React in the served shell) that: // (a) loads the compiled Phase-1 editor from the plugin's static public dir, // /plugins/public/theme-builder/builderApp.js (plugins.js:1246-1276), // (b) bootstraps window.__TB__ = { apiBase, cssRoute, csrfToken } -- the CSRF // token is read from req exactly the way core does it (req.csrfToken(), // e.g. server/wrapper.js:259, server/app.js:74 where it defaults to ""), // (c) exposes a single mount point
the editor // attaches to. // // The function RETURNS the complete document string; when a res is supplied // (the apiHandlers.getEditor path) it also res.send()s that document. We send a // standalone document rather than res.sendWrap()'ing a body fragment so the SPA // owns the whole page and stays free of the core React chrome -- per the task's // "COMPLETE HTML document / dependency-free shell" contract. const { API_BASE, CSS_ROUTE, URL_PREFIX, PLUGIN_NAME } = require("./constants"); // The plugin's committed editor bundle, served by core's static plugin route. const BUNDLE_URL = `/plugins/public/${PLUGIN_NAME}/builderApp.js`; // Mirror core's CSRF read: req.csrfToken is a function (csurf adds it; in some // modes app.js stubs it to return ""). Guard so a missing token never throws. function readCsrfToken(req) { if (req && typeof req.csrfToken === "function") { try { return req.csrfToken() || ""; } catch (e) { return ""; } } return ""; } // Minimal HTML-escaper for the and any attribute text. The JSON blob is // emitted via JSON.stringify with "<" escaped so it cannot break out of the // <script> element. function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """); } // JSON for inline <script>: prevent "</script>" / "<!--" from terminating the // element, and keep it ASCII-only. function safeJson(value) { return JSON.stringify(value) .replace(/</g, "\\u003c") .replace(/>/g, "\\u003e") .replace(/&/g, "\\u0026") .replace(/[\u2028]/g, "\\u2028") .replace(/[\u2029]/g, "\\u2029"); } // Build the complete HTML document string. openThemeId (?id=) lets a deep-link // open straight into a theme; the editor reads it from the boot blob. function buildShellHtml(req) { const csrfToken = readCsrfToken(req); const openThemeId = (req && req.query && req.query.id) ? String(req.query.id) : null; const boot = { apiBase: API_BASE, cssRoute: CSS_ROUTE, base: URL_PREFIX, csrfToken, openThemeId, }; return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="robots" content="noindex,nofollow"> <title>${escapeHtml("Theme Builder")}
Loading Theme Builder...
`; } // Public entry point. Returns the document string; if res is provided, sends it. function renderEditorShell(req, res) { const html = buildShellHtml(req); if (res && typeof res.send === "function") { res.set("Content-Type", "text/html; charset=utf-8"); res.send(html); } return html; } module.exports = { renderEditorShell, buildShellHtml, BUNDLE_URL };