Initial commit: code playground with multi-framework support
Express + SQLite backend with Monaco editor frontend. Supports HTML/CSS/JS, TypeScript, React (JSX/TSX), Vue SFC, and Svelte with live preview, console output, save/fork/share. Includes CSS preprocessors (SCSS, Less), framework-specific compilation (Babel, TypeScript, Svelte compiler), and CDN-loaded runtime libraries for preview rendering.
This commit is contained in:
204
public/js/js-preprocessors.js
Normal file
204
public/js/js-preprocessors.js
Normal file
@@ -0,0 +1,204 @@
|
||||
let tsLoaded = false;
|
||||
let babelLoaded = false;
|
||||
let svelteLoaded = false;
|
||||
|
||||
function loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; }
|
||||
// Temporarily hide AMD define so UMD scripts register as globals
|
||||
// instead of AMD modules (Monaco's RequireJS sets window.define)
|
||||
const savedDefine = window.define;
|
||||
window.define = undefined;
|
||||
const s = document.createElement('script');
|
||||
s.src = src;
|
||||
s.onload = () => { window.define = savedDefine; resolve(); };
|
||||
s.onerror = () => { window.define = savedDefine; reject(new Error(`Failed to load ${src}`)); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
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: '' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user