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">paste -d: names.txt numbers.txt | head -5</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">printf '🍒 cherry\n🍎 apple\n🍌 banana\n' | sort -k2</button>
<button class="playground-example">date</button>
+16 -10
View File
@@ -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);
});
})();
+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_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;
}
+3 -10
View File
@@ -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 =====
-1
View File
@@ -43,7 +43,6 @@
<a href="/bsdutils">Bsdutils</a>
</div>
</div>
<a href="/playground">Playground</a>
</div>
<div class="spacer"></div>
<div class="navigation-block">
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="wasm-example" data-command="{{ command }}">
<div class="wasm-example-code">
<pre><code>$ {{ command }}</code></pre>
<button class="wasm-run-btn" onclick="runWasmExample(this)">Run</button>
<button class="wasm-run-btn">Run</button>
</div>
<div class="wasm-example-output" style="display:none">
<pre></pre>