release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+6
View File
@@ -0,0 +1,6 @@
# @wrnexus/playground
A deployable, shareable `.wrn` playground with diagnostics, generated JavaScript,
safe SSR-shaped HTML, sandboxed preview, reactive/UI examples, and version adapters.
Run `wrnexus playground`, or deploy `createPlaygroundHandler()`.
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@wrnexus/playground",
"version": "0.8.0",
"type": "module",
"description": "Secure, shareable WRNexus compiler and UI playground.",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@wrnexus/compiler": "workspace:*",
"@wrnexus/syntax": "workspace:*"
}
}
+230
View File
@@ -0,0 +1,230 @@
import { compile, generateBrowserModule } from "@wrnexus/compiler";
import type { ViewNode } from "@wrnexus/syntax";
const escapeHtml = (value: string) =>
value.replace(
/[&<>"']/g,
(character) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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<Omit<PlaygroundCompilation, "source" | "version">>;
}
const DEFAULT_SOURCE = `component Counter {
state count = 0
functions { client function increment(): void { count++ } }
view { <button @click="increment">Count: {count}</button> }
}`;
function preview(nodes: ViewNode[]): string {
return nodes
.map((node) => {
if (node.type === "text")
return escapeHtml(node.value).replace(
/\{([^{}]+)\}/g,
'<span data-expression="$1">{$1}</span>',
);
if (node.type === "each")
return `<template data-each="${escapeHtml(node.list)}">${preview(node.body)}</template>`;
if (node.type === "if")
return `<template data-if="${escapeHtml(node.branches[0]?.cond ?? "else")}">${preview(node.branches[0]?.body ?? [])}</template>`;
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)}</${tag}>`;
})
.join("");
}
function literalState(ast: ReturnType<typeof compile>["ast"]): Record<string, unknown> {
const values: Record<string, unknown> = {};
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<typeof compile>["ast"]): Record<string, unknown> {
const output: Record<string, unknown> = {};
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<typeof compile>["ast"]): string {
const data = JSON.stringify({
state: literalState(ast),
operations: safeOperations(ast),
}).replace(/</g, "\\u003c");
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><style>body{font:16px system-ui;padding:1rem}</style></head><body>${fragment}<script>const model=${data};function draw(){document.querySelectorAll('[data-expression]').forEach(n=>{const k=n.dataset.expression.trim();if(Object.prototype.hasOwnProperty.call(model.state,k))n.textContent=String(model.state[k])})}document.addEventListener('click',e=>{const n=e.target.closest('[data-play-event-click]');if(!n)return;const op=model.operations[n.dataset.playEventClick];if(!op)return;if(op.operation==='++')model.state[op.state]++;else if(op.operation==='--')model.state[op.state]--;else if(op.operation==='+=')model.state[op.state]+=op.value;else if(op.operation==='-=')model.state[op.state]-=op.value;else model.state[op.state]=op.value;draw()});draw()</script></body></html>`;
}
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 `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>WRNexus Playground</title><style>body{font:14px system-ui;margin:0;padding:1rem;background:#111827;color:#f9fafb}main{display:grid;grid-template-columns:1fr 1fr;gap:1rem}textarea,pre,iframe{box-sizing:border-box;width:100%;min-height:18rem;background:#fff;color:#111;padding:.75rem;overflow:auto}button,select{padding:.6rem 1rem;margin:.25rem}section{min-width:0}@media(max-width:800px){main{grid-template-columns:1fr}}</style></head><body><h1>WRNexus Playground</h1><p>Write WRN, inspect SSR HTML/client/generated output, test components, share the URL, or attach it to a report.</p><label for="examples">Example</label><select id="examples" aria-label="Playground example"></select><button id="run">Compile</button><button id="compare">Compare versions</button><button id="share">Copy share URL</button> <span id="status" role="status"></span><main><section><h2>WRN source</h2><textarea id="source">${escapeHtml(source)}</textarea><h2>Sandboxed preview</h2><iframe id="preview" title="WRNexus component preview" sandbox="allow-scripts"></iframe></section><section><h2>SSR HTML</h2><pre id="html"></pre><h2>Generated JavaScript</h2><pre id="generated"></pre><h2>Client output</h2><pre id="client"></pre><h2>Diagnostics</h2><pre id="diagnostics"></pre><h2>Version comparisons</h2><pre id="versions"></pre></section></main><script src="/playground.js" defer></script></body></html>`;
}
export function createPlaygroundHandler(
options: { versions?: PlaygroundVersionAdapter[]; examples?: Record<string, string> } = {},
) {
return async (request: Request): Promise<Response> => {
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",
},
});
};
}
@@ -0,0 +1,68 @@
import { expect, test } from "bun:test";
import {
compilePlayground,
createPlaygroundHandler,
decodePlaygroundShare,
encodePlaygroundShare,
} from "../src/index.ts";
const source = `component Hello { props { name = "World" } view { <h1>Hello {name}</h1> } }`;
test("playground exposes generated, client, SSR-shaped and diagnostic output", () => {
const result = compilePlayground(source);
expect(result.generated).toContain("export function render");
expect(result.client).toContain("Hello");
expect(result.html).toContain("Hello");
expect(result.interactiveHtml).toContain("Content-Security-Policy");
expect(result.diagnostics).toEqual([]);
});
test("share URLs round-trip Unicode and enforce bounds", () => {
const unicode = `${source}\n// नमस्ते`;
expect(decodePlaygroundShare(encodePlaygroundShare(unicode))).toBe(unicode);
expect(() => encodePlaygroundShare("x".repeat(70_000))).toThrow("WRN-PLAYGROUND-SHARE-LIMIT");
});
test("deployable handler compiles and serves a sandboxed CSP page", async () => {
const handler = createPlaygroundHandler();
const response = await handler(
new Request("https://play.test/api/compile", {
method: "POST",
body: JSON.stringify({ source }),
}),
);
expect(response.status).toBe(200);
expect(await response.json()).toHaveProperty("share");
const html = await handler(new Request("https://play.test/"));
expect(html.headers.get("content-security-policy")).toContain("script-src 'self'");
const page = await html.text();
expect(page).toContain('sandbox="allow-scripts"');
expect(page).toContain('id="compare"');
expect(page).toContain('id="examples"');
});
test("handler exposes examples and optional version comparisons", async () => {
const handler = createPlaygroundHandler({
examples: { hello: source },
versions: [{ version: "0.7.0", compile: async () => compilePlayground(source, "0.7.0") }],
});
const examples = await handler(new Request("https://play.test/api/examples"));
expect(await examples.json()).toEqual({ hello: source });
const comparison = await handler(
new Request("https://play.test/api/compile", {
method: "POST",
body: JSON.stringify({ source, versions: true }),
}),
);
const result = (await comparison.json()) as { comparisons: Array<{ version: string }> };
expect(result.comparisons[0]?.version).toBe("0.7.0");
});
test("reactive preview interprets only bounded state operations", () => {
const result = compilePlayground(
`component Counter {
state count = 0
functions { client function increment(): void { count++ } }
view { <button @click="increment">{count}</button> }
}`,
);
expect(result.interactiveHtml).toContain('"operation":"++"');
expect(result.interactiveHtml).toContain("data-play-event-click");
});