Files
NexusOS/interface/web/src/preview/languages.js
T
AthenaandCursor 00bd43d32e feat(preview): add sandboxed live code previews
Render validated HTML, SVG, JSX, and TSX fences locally while preserving tool context and preventing explanatory JSON from triggering actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 00:51:29 -05:00

100 lines
3.7 KiB
JavaScript

/*
* languages.js — what the render window can preview, one entry per language.
*
* Each entry turns a fence's contents into the <body> of the sandboxed frame:
*
* toBody(value) -> { html, userOffset }
*
* `userOffset` is how many lines of that body come before the user's own code.
* The frame reports runtime errors by line number and those numbers are
* document-relative, so without this an error in a JSX component would be
* reported at some line deep inside the inlined Preact build. The caller adds
* the lines of document shell above the body and hands the total to the
* bootstrap, which subtracts it before reporting.
*
* A `toBody` may throw: JSX that doesn't parse has no preview to show. The
* caller catches and shows the message in place of the frame.
*
* The backend keeps a matching registry (PREVIEW_LANGS in synapse/tools.py)
* that says how each language is *validated* rather than rendered. Neither
* depends on the other at runtime; tests/test_tools.py asserts the key sets
* stay equal.
*/
import { transform } from "./jsx-transform.js";
import { PREACT_RUNTIME } from "./runtime.js";
const countNewlines = (text) => (text.match(/\n/g) || []).length;
/** Markup languages: the fence is already a document body. */
const markup = (value) => ({ html: value, userOffset: 0 });
/**
* Pick what to mount. An explicit default export wins, then a component named
* App, then the last capitalized declaration - models tend to define helpers
* first and the thing they were asked for last.
*/
function mountTarget({ defaultExport, components }) {
if (defaultExport) return defaultExport;
if (components.includes("App")) return "App";
if (components.length) return components[components.length - 1];
throw new Error(
"No component found to render. Name one `App`, or `export default` it.",
);
}
/**
* One line of stubs for every binding an import would have provided.
*
* Imports are dropped — there is no module loader in the sandbox — so a name
* that came from a package is simply missing, and the first use of it reports
* "useInView is not defined" at a line far from the import that explains it.
* Each stub throws with the module name instead, and `||` means anything the
* runtime already provides (useState and friends) keeps its real implementation.
*/
function importStubs(imports) {
const names = new Map();
for (const { names: bound, module } of imports || []) {
for (const name of bound) if (!names.has(name)) names.set(name, module);
}
if (!names.size) return "";
const lines = [...names].map(([name, module]) => {
const why = JSON.stringify(
`${name} came from ${module ? `"${module}"` : "an import"}, which the preview ` +
"cannot load — it has no module loader and no network. Inline what you need, " +
"or use the built-in hooks, which are already in scope.",
);
return `window[${JSON.stringify(name)}] = window[${JSON.stringify(name)}] ` +
`|| function () { throw new Error(${why}); };`;
});
return `<script>${lines.join("")}</script>\n`;
}
function jsxBody(value) {
const result = transform(value);
const target = mountTarget(result);
const head =
'<div id="root"></div>\n' +
`<script>${PREACT_RUNTIME}</script>\n` +
importStubs(result.imports) +
"<script>\n";
return {
html:
head +
result.code +
`\n;render(h(${target}, null), document.getElementById("root"));\n` +
"</script>",
userOffset: countNewlines(head),
};
}
export const PREVIEW_LANGS = {
html: { toBody: markup },
svg: { toBody: markup },
jsx: { toBody: jsxBody },
tsx: { toBody: jsxBody },
};
export const RENDERABLE_LANGS = new Set(Object.keys(PREVIEW_LANGS));