Files
fiddle/public/js/js-preprocessors.js
root e41c3e7dc4 Add browse dashboard, tags, visibility control, export, QR sharing, and embed mode
- Browse dashboard at / with search, framework filter, tag pills, and pagination
- Tags system with autocomplete datalist and per-fiddle tag management
- Listed/unlisted toggle for visibility control (unlisted still accessible via direct URL)
- Export standalone HTML with inlined CSS/JS and framework CDN tags
- QR code modal for sharing fiddle URLs
- Embed mode at /embed/:id for minimal preview-only rendering
- Extract shared loadScript() utility from 4 files into utils.js
- Database schema: listed column, tags and fiddle_tags tables with index
2026-02-26 14:19:52 -06:00

192 lines
5.1 KiB
JavaScript

import { loadScript } from './utils.js';
let tsLoaded = false;
let babelLoaded = false;
let svelteLoaded = false;
async function ensureTypeScript() {
if (tsLoaded) return;
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/typescript/5.6.3/typescript.min.js');
if (typeof ts === 'undefined' && typeof window.ts === 'undefined') {
await new Promise((resolve) => {
const check = setInterval(() => {
if (typeof ts !== 'undefined') { clearInterval(check); resolve(); }
}, 50);
});
}
tsLoaded = true;
}
async function ensureBabel() {
if (babelLoaded) return;
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.4/babel.min.js');
// Wait for global to be available
if (typeof Babel === 'undefined') {
await new Promise((resolve) => {
const check = setInterval(() => {
if (typeof Babel !== 'undefined') { clearInterval(check); resolve(); }
}, 50);
});
}
babelLoaded = true;
}
async function ensureSvelte() {
if (svelteLoaded) return;
await loadScript('https://unpkg.com/svelte@4.2.19/compiler.cjs');
svelteLoaded = true;
}
/**
* Compile JS/TS/JSX/TSX/Vue/Svelte code based on mode.
* Returns { js, extraCss?, warnings? }
*/
export async function compileJs(code, mode) {
if (!code.trim()) return { js: '' };
switch (mode) {
case 'html-css-js':
return { js: code };
case 'typescript':
return compileTypeScript(code);
case 'react':
return compileJsx(code);
case 'react-ts':
return compileTsx(code);
case 'vue':
return compileVue(code);
case 'svelte':
return compileSvelte(code);
default:
return { js: code };
}
}
async function compileTypeScript(code) {
await ensureTypeScript();
const result = ts.transpileModule(code, {
compilerOptions: {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.None,
strict: false,
},
});
return { js: result.outputText };
}
async function compileJsx(code) {
await ensureBabel();
const result = Babel.transform(code, {
presets: ['react'],
filename: 'fiddle.jsx',
});
return { js: result.code };
}
async function compileTsx(code) {
await ensureBabel();
const result = Babel.transform(code, {
presets: ['react', 'typescript'],
filename: 'fiddle.tsx',
});
return { js: result.code };
}
function compileVue(code) {
// Simple regex-based SFC parser
const templateMatch = code.match(/<template>([\s\S]*?)<\/template>/);
const scriptMatch = code.match(/<script>([\s\S]*?)<\/script>/);
const styleMatch = code.match(/<style(?:\s[^>]*)?>([\s\S]*?)<\/style>/);
const template = templateMatch ? templateMatch[1].trim() : '<div></div>';
const script = scriptMatch ? scriptMatch[1].trim() : '';
const extraCss = styleMatch ? styleMatch[1].trim() : '';
// Build the Vue component + mount code
// The user script should export/define component options
const js = `
(function() {
${script}
// Detect if user defined a component variable
var _opts = typeof component !== 'undefined' ? component : {};
_opts.template = ${JSON.stringify(template)};
Vue.createApp(_opts).mount('#app');
})();
`;
return { js, extraCss };
}
async function compileSvelte(code) {
await ensureSvelte();
const result = svelte.compile(code, {
filename: 'App.svelte',
css: 'injected',
});
// Rewrite all svelte imports for browser use via esm.sh
// Handles both `import {...} from "svelte/..."` and `import "svelte/..."`
let js = result.js.code;
js = js.replace(
/from\s+["']svelte(\/[^"']*)?["']/g,
(_, path) => `from "https://esm.sh/svelte@4${path || ''}"`
);
js = js.replace(
/import\s+["']svelte(\/[^"']*)?["']/g,
(_, path) => `import "https://esm.sh/svelte@4${path || ''}"`
);
// Remove the default export and add mount code
// The compiled output exports the class as "export default class App ..."
// We replace the export to keep it as a local class, then instantiate it
js = js.replace(/export default /, '');
// Add mount code — the class name matches the filename ("App")
js += `\n\nnew App({ target: document.getElementById('app') });\n`;
const warnings = result.warnings ? result.warnings.map((w) => w.message) : [];
return { js, warnings, isModule: true };
}
/**
* Get runtime scripts and body HTML to inject into the preview iframe.
*/
export function getFrameworkRuntime(mode) {
switch (mode) {
case 'react':
case 'react-ts':
return {
scripts: [
'https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js',
],
bodyHtml: '<div id="root"></div>',
};
case 'vue':
return {
scripts: [
'https://cdnjs.cloudflare.com/ajax/libs/vue/3.5.13/vue.global.prod.min.js',
],
bodyHtml: '<div id="app"></div>',
};
case 'svelte':
return {
scripts: [],
bodyHtml: '<div id="app"></div>',
};
default:
return { scripts: [], bodyHtml: '' };
}
}