forked from enderofwings/NexusOS
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>
100 lines
3.7 KiB
JavaScript
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));
|