import { compile, generateBrowserModule } from "@wrnexus/compiler"; import type { ViewNode } from "@wrnexus/syntax"; const escapeHtml = (value: string) => value.replace( /[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]!, ); export interface PlaygroundCompilation { source: string; generated: string; client: string; html: string; interactiveHtml: string; diagnostics: Array<{ code: string; message: string; severity: string }>; version: string; } export interface PlaygroundVersionAdapter { version: string; compile(source: string): Promise>; } const DEFAULT_SOURCE = `component Counter { state count = 0 functions { client function increment(): void { count++ } } view { } }`; function preview(nodes: ViewNode[]): string { return nodes .map((node) => { if (node.type === "text") return escapeHtml(node.value).replace( /\{([^{}]+)\}/g, '{$1}', ); if (node.type === "each") return ``; if (node.type === "if") return ``; const tag = /^[a-z][a-z0-9-]*$/.test(node.tag) ? node.tag : "div"; const attrs = node.attrs .filter((attribute) => !/^(?:srcdoc|on\w+)$/i.test(attribute.name)) .map((attribute) => attribute.event ? ` data-play-event-${attribute.name}="${escapeHtml(attribute.value)}"` : attribute.boolean ? ` ${attribute.name}` : ` ${attribute.name}="${escapeHtml(attribute.value)}"`, ) .join(""); return `<${tag}${attrs}>${preview(node.children)}`; }) .join(""); } function literalState(ast: ReturnType["ast"]): Record { const values: Record = {}; for (const state of ast.states) { const value = state.expr.trim(); if (/^-?\d+(?:\.\d+)?$/.test(value)) values[state.name] = Number(value); else if (value === "true" || value === "false") values[state.name] = value === "true"; else if (value === "null") values[state.name] = null; else if (/^(["']).*\1$/.test(value)) values[state.name] = value.slice(1, -1); } return values; } function safeOperations(ast: ReturnType["ast"]): Record { const output: Record = {}; const states = new Set(ast.states.map((state) => state.name)); for (const fn of ast.runtimeFunctions.filter((entry) => entry.runtime !== "server")) { const body = fn.body.trim().replace(/;$/, ""); const unary = /^([A-Za-z_$][\w$]*)(\+\+|--)$/.exec(body); const assignment = /^([A-Za-z_$][\w$]*)\s*(\+=|-=|=)\s*(-?\d+(?:\.\d+)?|true|false|(["']).*\4)$/.exec(body); const match = unary ?? assignment; if (!match || !states.has(match[1]!)) continue; const raw = assignment?.[3]; const value = raw === undefined ? undefined : /^-?\d/.test(raw) ? Number(raw) : raw === "true" || raw === "false" ? raw === "true" : raw.slice(1, -1); output[fn.name] = { state: match[1]!, operation: match[2]!, ...(value !== undefined ? { value } : {}), }; } return output; } function interactiveDocument(fragment: string, ast: ReturnType["ast"]): string { const data = JSON.stringify({ state: literalState(ast), operations: safeOperations(ast), }).replace(/${fragment}`; } export function compilePlayground(source: string, version = "0.8.0"): PlaygroundCompilation { if (new TextEncoder().encode(source).byteLength > 128 * 1024) throw new RangeError("WRN-PLAYGROUND-SOURCE-LIMIT"); const result = compile(source, "playground.wrn"); const html = preview(result.ast.view); return { source, generated: result.code, client: generateBrowserModule(result.ast), html, interactiveHtml: interactiveDocument(html, result.ast), diagnostics: result.richDiagnostics.map(({ code, message, severity }) => ({ code, message, severity, })), version, }; } export function encodePlaygroundShare(source: string): string { const bytes = new TextEncoder().encode(source); if (bytes.byteLength > 64 * 1024) throw new RangeError("WRN-PLAYGROUND-SHARE-LIMIT"); let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } export function decodePlaygroundShare(value: string): string { if (!/^[A-Za-z0-9_-]{1,100000}$/.test(value)) throw new Error("WRN-PLAYGROUND-SHARE: invalid payload"); const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/")); const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); if (bytes.byteLength > 64 * 1024) throw new RangeError("WRN-PLAYGROUND-SHARE-LIMIT"); return new TextDecoder().decode(bytes); } export async function comparePlaygroundVersions( source: string, adapters: PlaygroundVersionAdapter[], ) { return { current: compilePlayground(source), comparisons: await Promise.all( adapters.map(async (adapter) => ({ version: adapter.version, ...(await adapter.compile(source)), })), ), }; } const CLIENT = String.raw` const source=document.querySelector('#source'),status=document.querySelector('#status'),examples=document.querySelector('#examples'); async function run(versions=false){status.textContent=versions?'Comparing':'Compiling';const response=await fetch('/api/compile',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({source:source.value,versions})});const value=await response.json(),current=value.current||value;status.textContent=response.ok?(versions?'Compared':'Compiled'):'Failed';for(const name of ['html','generated','client','diagnostics'])document.querySelector('#'+name).textContent=typeof current[name]==='string'?current[name]:JSON.stringify(current[name]||[],null,2);document.querySelector('#versions').textContent=JSON.stringify(value.comparisons||[],null,2);document.querySelector('#preview').srcdoc=current.interactiveHtml||current.html||'';if(value.share)history.replaceState(null,'','?code='+value.share)} async function loadExamples(){const response=await fetch('/api/examples'),value=await response.json();for(const [name,code] of Object.entries(value)){const option=document.createElement('option');option.value=name;option.textContent=name;examples.append(option);examples.dataset[name]=code}if(examples.options.length)examples.dispatchEvent(new Event('change'))} document.querySelector('#run').addEventListener('click',()=>run()); document.querySelector('#compare').addEventListener('click',()=>run(true)); examples.addEventListener('change',()=>{const value=examples.dataset[examples.value];if(value)source.value=value}); document.querySelector('#share').addEventListener('click',()=>navigator.clipboard&&navigator.clipboard.writeText(location.href)); loadExamples().catch(()=>{status.textContent='Examples unavailable'}); `; function page(source: string) { return `WRNexus Playground

WRNexus Playground

Write WRN, inspect SSR HTML/client/generated output, test components, share the URL, or attach it to a report.

WRN source

Sandboxed preview

SSR HTML

Generated JavaScript

Client output

Diagnostics

Version comparisons

`; } export function createPlaygroundHandler( options: { versions?: PlaygroundVersionAdapter[]; examples?: Record } = {}, ) { return async (request: Request): Promise => { const url = new URL(request.url); if (url.pathname === "/playground.js") return new Response(CLIENT, { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "public, max-age=3600", "x-content-type-options": "nosniff", }, }); if (url.pathname === "/api/examples") return Response.json(options.examples ?? { counter: DEFAULT_SOURCE }); if (url.pathname === "/api/compile" && request.method === "POST") { try { const text = await request.text(); if (new TextEncoder().encode(text).byteLength > 140 * 1024) return Response.json({ error: "Payload too large" }, { status: 413 }); const body = JSON.parse(text) as { source?: unknown; versions?: boolean }; if (typeof body.source !== "string") return Response.json({ error: "source is required" }, { status: 400 }); const result = body.versions && options.versions?.length ? await comparePlaygroundVersions(body.source, options.versions) : compilePlayground(body.source); return Response.json( { ...result, share: encodePlaygroundShare(body.source) }, { headers: { "cache-control": "no-store" } }, ); } catch (error) { return Response.json( { error: error instanceof Error ? error.message : "Compilation failed" }, { status: 400 }, ); } } if (url.pathname !== "/") return new Response("Not Found", { status: 404 }); let source = DEFAULT_SOURCE; const encoded = url.searchParams.get("code"); if (encoded) { try { source = decodePlaygroundShare(encoded); } catch { source = DEFAULT_SOURCE; } } return new Response(page(source), { headers: { "content-type": "text/html; charset=utf-8", "content-security-policy": "default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; frame-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'", "x-frame-options": "SAMEORIGIN", "x-content-type-options": "nosniff", "referrer-policy": "no-referrer", }, }); }; }