Load Sucrase only when a JSX/TSX preview is opened, remove the hand-written transform, and leave subjective render evaluation to the reader while retaining structural fence validation.
87 lines
3.3 KiB
JavaScript
87 lines
3.3 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:
|
|
*
|
|
* await 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)
|
|
* for tool descriptions and language tags. 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 });
|
|
|
|
/**
|
|
* Build the mount expression. 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 mountExpression(components) {
|
|
const names = ["App", ...components.slice().reverse()]
|
|
.filter((name, index, all) => all.indexOf(name) === index);
|
|
const lexical = names.map(
|
|
(name) => `(typeof ${name} !== "undefined" ? ${name} : null)`,
|
|
);
|
|
return [
|
|
"module.exports.default",
|
|
"module.exports.App",
|
|
...lexical,
|
|
"Object.values(module.exports).find((value) => typeof value === 'function')",
|
|
].join(" || ");
|
|
}
|
|
|
|
async function jsxBody(value) {
|
|
const result = await transform(value);
|
|
const target = mountExpression(result.components);
|
|
|
|
const head =
|
|
'<div id="root"></div>\n' +
|
|
`<script>${PREACT_RUNTIME}</script>\n` +
|
|
"<script>\n" +
|
|
"const module = { exports: {} }; const exports = module.exports;\n" +
|
|
"const require = (name) => {\n" +
|
|
" const modules = { react: React, 'react-dom': ReactDOM, preact, 'preact/hooks': preactHooks };\n" +
|
|
" if (Object.prototype.hasOwnProperty.call(modules, name)) return modules[name];\n" +
|
|
" throw new Error(`Cannot import '${name}' — the preview has no module loader or network.`);\n" +
|
|
"};\n";
|
|
|
|
return {
|
|
html:
|
|
head +
|
|
result.code +
|
|
`\n;const __NexusComponent = ${target};\n` +
|
|
"if (!__NexusComponent) throw new Error(" +
|
|
"'No component found to render. Name one `App`, or `export default` it.');\n" +
|
|
"const __NexusView = typeof __NexusComponent === 'function' " +
|
|
"? h(__NexusComponent, null) : __NexusComponent;\n" +
|
|
"render(__NexusView, 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));
|