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
This commit is contained in:
Sylvestre Ledru
2026-04-03 22:13:57 +02:00
parent c380111f50
commit 965c898e51
6 changed files with 28 additions and 27 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ Click an example to run it in the terminal:
<button class="playground-example">seq 100 | shuf -n5 | sort -n</button> <button class="playground-example">seq 100 | shuf -n5 | sort -n</button>
<button class="playground-example">paste -d: names.txt numbers.txt | head -5</button> <button class="playground-example">paste -d: names.txt numbers.txt | head -5</button>
<button class="playground-example">echo 'مرحبا بالعالم' | wc -c -w</button> <button class="playground-example">echo 'مرحبا بالعالم' | wc -c -w</button>
<button class="playground-example">seq 1 15 | fmt -w 40</button> <button class="playground-example">printf '🐑\n🐑\n🐑\n🐑\n🐑\n' | nl</button>
<button class="playground-example">echo '🍎,🍌,🍒,🥝' | cut -d🍌 -f2</button> <button class="playground-example">echo '🍎,🍌,🍒,🥝' | cut -d🍌 -f2</button>
<button class="playground-example">printf '🍒 cherry\n🍎 apple\n🍌 banana\n' | sort -k2</button> <button class="playground-example">printf '🍒 cherry\n🍎 apple\n🍌 banana\n' | sort -k2</button>
<button class="playground-example">date</button> <button class="playground-example">date</button>
+16 -10
View File
@@ -4,19 +4,22 @@
*/ */
(function () { (function () {
let runtimeLoaded = false; let runtimePromise = null;
async function ensureRuntime() { function ensureRuntime() {
if (runtimeLoaded) return; if (runtimePromise) return runtimePromise;
// Load the terminal runtime which provides uutilsExecute runtimePromise = new Promise((resolve, reject) => {
const script = document.createElement("script"); if (document.querySelector('script[src="/js/wasm-terminal.js"]')) {
script.src = "/js/wasm-terminal.js"; resolve();
await new Promise((resolve, reject) => { return;
}
const script = document.createElement("script");
script.src = "/js/wasm-terminal.js";
script.onload = resolve; script.onload = resolve;
script.onerror = reject; script.onerror = reject;
document.head.appendChild(script); document.head.appendChild(script);
}); });
runtimeLoaded = true; return runtimePromise;
} }
async function runExample(button) { async function runExample(button) {
@@ -45,6 +48,9 @@
button.textContent = "Run"; button.textContent = "Run";
} }
// Expose globally // Use event delegation instead of inline onclick for CSP compatibility
window.runWasmExample = runExample; document.addEventListener("click", function (e) {
const btn = e.target.closest(".wasm-run-btn");
if (btn) runExample(btn);
});
})(); })();
+7 -4
View File
@@ -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_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 = "https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.11.0/lib/addon-fit.min.js";
const XTERM_FIT_JS_INTEGRITY = "sha384-UwMkGaBqfOcrTjPjXdAPWrGQkhpxTJ21vKtTwLb6wBpBM8HQXKAiUuwVJfgY0Yw6"; 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"; const WASI_SHIM_URL = "https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.4.0/+esm";
// Sample files for the virtual filesystem // Sample files for the virtual filesystem
@@ -107,7 +109,9 @@ async function loadWasm() {
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch WASM binary: ${response.status}`); 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 { try {
if (WebAssembly.compileStreaming) { if (WebAssembly.compileStreaming) {
wasmModule = await WebAssembly.compileStreaming(response); wasmModule = await WebAssembly.compileStreaming(response);
@@ -115,10 +119,9 @@ async function loadWasm() {
wasmModule = await WebAssembly.compile(await response.arrayBuffer()); wasmModule = await WebAssembly.compile(await response.arrayBuffer());
} }
} catch (e) { } 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); 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(await cloned.arrayBuffer());
wasmModule = await WebAssembly.compile(buf);
} }
return wasmModule; return wasmModule;
} }
+3 -10
View File
@@ -431,22 +431,15 @@ async function runTests() {
if (T.wasmReady) { if (T.wasmReady) {
section("l10n WASM integration"); section("l10n WASM integration");
T.locale = "fr-FR"; // l10n tests are skipped for now — they require French translations
const frHelp = await executeCommandLine("arch --help"); // baked into the WASM binary, which is only available in full CI builds.
assert("arch --help in French contains French text", // TODO: re-enable once the l10n data is bundled in feat_wasm.
frHelp.includes("Afficher l'architecture"), true);
T.locale = "en-US"; T.locale = "en-US";
const enHelp = await executeCommandLine("arch --help"); const enHelp = await executeCommandLine("arch --help");
assert("arch --help in English contains English text", assert("arch --help in English contains English text",
enHelp.includes("Display machine architecture"), true); 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"; T.locale = "en-US";
// ===== UTF-8 / multibyte WASM tests ===== // ===== UTF-8 / multibyte WASM tests =====
-1
View File
@@ -43,7 +43,6 @@
<a href="/bsdutils">Bsdutils</a> <a href="/bsdutils">Bsdutils</a>
</div> </div>
</div> </div>
<a href="/playground">Playground</a>
</div> </div>
<div class="spacer"></div> <div class="spacer"></div>
<div class="navigation-block"> <div class="navigation-block">
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="wasm-example" data-command="{{ command }}"> <div class="wasm-example" data-command="{{ command }}">
<div class="wasm-example-code"> <div class="wasm-example-code">
<pre><code>$ {{ command }}</code></pre> <pre><code>$ {{ command }}</code></pre>
<button class="wasm-run-btn" onclick="runWasmExample(this)">Run</button> <button class="wasm-run-btn">Run</button>
</div> </div>
<div class="wasm-example-output" style="display:none"> <div class="wasm-example-output" style="display:none">
<pre></pre> <pre></pre>