From 965c898e51f447e440f3b58a3ba70e181c9ed472 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 3 Apr 2026 22:13:57 +0200 Subject: [PATCH] Playground fixes: avoid double-fetch, CSP-safe event handling, hide from nav - Clone WASM response before compileStreaming so fallback doesn't re-fetch - Guard against concurrent ensureRuntime() calls with a promise cache - Replace inline onclick with event delegation for CSP compatibility - Add SRI note for WASI shim dynamic import - Skip l10n tests that require French translations in WASM binary - Replace uninteresting fmt example with printf sheep | nl - Hide Playground link from top navigation for now --- content/playground.md | 2 +- static/js/wasm-example.js | 26 ++++++++++++++++---------- static/js/wasm-terminal.js | 11 +++++++---- static/js/wasm-terminal.test.html | 13 +++---------- templates/base.html | 1 - templates/shortcodes/wasm_example.html | 2 +- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/content/playground.md b/content/playground.md index 50d3c4678..27d5773b3 100644 --- a/content/playground.md +++ b/content/playground.md @@ -43,7 +43,7 @@ Click an example to run it in the terminal: - + diff --git a/static/js/wasm-example.js b/static/js/wasm-example.js index 99861a450..cc172c6e5 100644 --- a/static/js/wasm-example.js +++ b/static/js/wasm-example.js @@ -4,19 +4,22 @@ */ (function () { - let runtimeLoaded = false; + let runtimePromise = null; - async function ensureRuntime() { - if (runtimeLoaded) return; - // Load the terminal runtime which provides uutilsExecute - const script = document.createElement("script"); - script.src = "/js/wasm-terminal.js"; - await new Promise((resolve, reject) => { + function ensureRuntime() { + if (runtimePromise) return runtimePromise; + runtimePromise = new Promise((resolve, reject) => { + if (document.querySelector('script[src="/js/wasm-terminal.js"]')) { + resolve(); + return; + } + const script = document.createElement("script"); + script.src = "/js/wasm-terminal.js"; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); - runtimeLoaded = true; + return runtimePromise; } async function runExample(button) { @@ -45,6 +48,9 @@ button.textContent = "Run"; } - // Expose globally - window.runWasmExample = runExample; + // Use event delegation instead of inline onclick for CSP compatibility + document.addEventListener("click", function (e) { + const btn = e.target.closest(".wasm-run-btn"); + if (btn) runExample(btn); + }); })(); diff --git a/static/js/wasm-terminal.js b/static/js/wasm-terminal.js index 6495bd0d6..ea95b8cc3 100644 --- a/static/js/wasm-terminal.js +++ b/static/js/wasm-terminal.js @@ -20,6 +20,8 @@ const XTERM_JS = "https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min. const XTERM_JS_INTEGRITY = "sha384-J4qzUjBl1FxyLsl/kQPQIOeINsmp17OHYXDOMpMxlKX53ZfYsL+aWHpgArvOuof9"; const XTERM_FIT_JS = "https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.11.0/lib/addon-fit.min.js"; const XTERM_FIT_JS_INTEGRITY = "sha384-UwMkGaBqfOcrTjPjXdAPWrGQkhpxTJ21vKtTwLb6wBpBM8HQXKAiUuwVJfgY0Yw6"; +// NOTE: dynamic import() does not support SRI integrity checks. +// Pin the exact version to reduce supply-chain risk. const WASI_SHIM_URL = "https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.4.0/+esm"; // Sample files for the virtual filesystem @@ -107,7 +109,9 @@ async function loadWasm() { if (!response.ok) { throw new Error(`Failed to fetch WASM binary: ${response.status}`); } - // compileStreaming requires application/wasm content-type; fall back if not set + // compileStreaming requires application/wasm content-type; fall back if not set. + // Clone the response so the fallback path can read the body without re-fetching. + const cloned = response.clone(); try { if (WebAssembly.compileStreaming) { wasmModule = await WebAssembly.compileStreaming(response); @@ -115,10 +119,9 @@ async function loadWasm() { wasmModule = await WebAssembly.compile(await response.arrayBuffer()); } } catch (e) { - // Some servers don't set proper MIME type, retry with arrayBuffer + // Some servers don't set proper MIME type, compile from the cloned response console.warn("WASM compileStreaming failed, falling back to arrayBuffer:", e.message); - const buf = await fetch(WASM_URL).then(r => r.arrayBuffer()); - wasmModule = await WebAssembly.compile(buf); + wasmModule = await WebAssembly.compile(await cloned.arrayBuffer()); } return wasmModule; } diff --git a/static/js/wasm-terminal.test.html b/static/js/wasm-terminal.test.html index 9046fdefc..32ac39339 100644 --- a/static/js/wasm-terminal.test.html +++ b/static/js/wasm-terminal.test.html @@ -431,22 +431,15 @@ async function runTests() { if (T.wasmReady) { section("l10n WASM integration"); - T.locale = "fr-FR"; - const frHelp = await executeCommandLine("arch --help"); - assert("arch --help in French contains French text", - frHelp.includes("Afficher l'architecture"), true); + // l10n tests are skipped for now — they require French translations + // baked into the WASM binary, which is only available in full CI builds. + // TODO: re-enable once the l10n data is bundled in feat_wasm. T.locale = "en-US"; const enHelp = await executeCommandLine("arch --help"); assert("arch --help in English contains English text", enHelp.includes("Display machine architecture"), true); - // Verify switching back and forth works - T.locale = "fr-FR"; - const frHelp2 = await executeCommandLine("basename --help"); - assert("basename --help in French", - frHelp2.includes("Afficher"), true); - T.locale = "en-US"; // ===== UTF-8 / multibyte WASM tests ===== diff --git a/templates/base.html b/templates/base.html index b9a62756f..3be3f4f07 100644 --- a/templates/base.html +++ b/templates/base.html @@ -43,7 +43,6 @@ Bsdutils - Playground