Playground#309
Conversation
BrowserPod currently requires the usage of is own openssl build for networking. This will change in the future and this can be reverted then.
linux-raw-sys does not support the BrowserPod target. In the future a patched version will be provided with the BrowserPod rust toolchain, and this could be reverted.
BrowserPod matches target_arch = "wasm64"
✅ Deploy Preview for yarn-v6 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: API key logged to console
- Removed console.log statement that was exposing the BrowserPod API key to browser devtools.
- ✅ Fixed: Share always shows Copied
- Removed error handler and fallback that incorrectly showed 'Copied' on clipboard failures, now only shows on success.
Or push these changes by commenting:
@cursor push b12af016ac
Preview (b12af016ac)
diff --git a/website/src/components/playground/PlaygroundTerminal.tsx b/website/src/components/playground/PlaygroundTerminal.tsx
--- a/website/src/components/playground/PlaygroundTerminal.tsx
+++ b/website/src/components/playground/PlaygroundTerminal.tsx
@@ -224,7 +224,6 @@
writeLines(term, [`${cyan}[browserpod]${reset} Booting pod...`]);
- console.log(`Booting pod...`, apiKey);
const pod = await BrowserPod.boot({apiKey});
if (disposed || !term)
diff --git a/website/src/pages/playground.astro b/website/src/pages/playground.astro
--- a/website/src/pages/playground.astro
+++ b/website/src/pages/playground.astro
@@ -56,9 +56,7 @@
};
if (navigator.clipboard && window.isSecureContext)
- navigator.clipboard.writeText(location.href).then(done, done);
- else
- done();
+ navigator.clipboard.writeText(location.href).then(done);
});
})();
</script>You can send follow-ups to the cloud agent here.
⏱️ Benchmark Resultsgatsby install-full-cold
📊 Raw benchmark data (gatsby install-full-cold)Base times: 4.344s, 4.496s, 4.429s, 4.306s, 4.237s, 4.365s, 4.422s, 4.331s, 4.422s, 4.395s, 4.429s, 4.418s, 4.396s, 4.343s, 4.434s, 4.447s, 4.297s, 4.368s, 4.320s, 4.379s, 4.408s, 4.457s, 4.409s, 4.374s, 4.442s, 4.417s, 4.398s, 4.375s, 4.450s, 4.453s Head times: 4.402s, 4.401s, 4.377s, 4.348s, 4.313s, 4.398s, 4.350s, 4.395s, 4.400s, 4.318s, 4.343s, 4.377s, 4.378s, 4.378s, 4.361s, 4.449s, 4.475s, 4.353s, 4.362s, 4.191s, 4.356s, 4.361s, 4.357s, 4.310s, 4.378s, 4.226s, 4.400s, 4.462s, 4.366s, 4.461s gatsby install-cache-only
📊 Raw benchmark data (gatsby install-cache-only)Base times: 1.273s, 1.280s, 1.282s, 1.289s, 1.275s, 1.276s, 1.260s, 1.274s, 1.282s, 1.279s, 1.268s, 1.287s, 1.296s, 1.287s, 1.272s, 1.277s, 1.284s, 1.273s, 1.280s, 1.290s, 1.291s, 1.280s, 1.312s, 1.280s, 1.273s, 1.273s, 1.259s, 1.258s, 1.267s, 1.273s Head times: 1.275s, 1.265s, 1.267s, 1.279s, 1.290s, 1.279s, 1.283s, 1.278s, 1.285s, 1.281s, 1.285s, 1.269s, 1.266s, 1.280s, 1.265s, 1.258s, 1.282s, 1.266s, 1.262s, 1.275s, 1.280s, 1.288s, 1.279s, 1.262s, 1.272s, 1.278s, 1.264s, 1.269s, 1.278s, 1.272s gatsby install-cache-and-lock (warm, with lockfile)
📊 Raw benchmark data (gatsby install-cache-and-lock (warm, with lockfile))Base times: 0.348s, 0.351s, 0.349s, 0.350s, 0.346s, 0.348s, 0.343s, 0.341s, 0.348s, 0.343s, 0.345s, 0.342s, 0.344s, 0.343s, 0.338s, 0.347s, 0.347s, 0.352s, 0.338s, 0.339s, 0.339s, 0.344s, 0.343s, 0.343s, 0.343s, 0.346s, 0.339s, 0.344s, 0.338s, 0.341s Head times: 0.346s, 0.342s, 0.343s, 0.343s, 0.341s, 0.340s, 0.344s, 0.342s, 0.346s, 0.355s, 0.344s, 0.342s, 0.342s, 0.344s, 0.343s, 0.341s, 0.343s, 0.342s, 0.342s, 0.343s, 0.347s, 0.343s, 0.344s, 0.340s, 0.347s, 0.344s, 0.343s, 0.345s, 0.343s, 0.342s |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: BrowserPod sessions leak on remount
- Added disposed check after writeProjectFiles and stored pod reference for cleanup to prevent multiple BrowserPod instances from running simultaneously.
Or push these changes by commenting:
@cursor push 1370c6a18b
Preview (1370c6a18b)
diff --git a/website/src/components/playground/PlaygroundTerminal.tsx b/website/src/components/playground/PlaygroundTerminal.tsx
--- a/website/src/components/playground/PlaygroundTerminal.tsx
+++ b/website/src/components/playground/PlaygroundTerminal.tsx
@@ -1,6 +1,6 @@
-import '@xterm/xterm/css/xterm.css';
-import type {Terminal as XtermTerminal} from '@xterm/xterm';
-import {useEffect, useRef} from 'react';
+import "@xterm/xterm/css/xterm.css";
+import type { Terminal as XtermTerminal } from "@xterm/xterm";
+import { useEffect, useRef } from "react";
interface Props {
files: Array<PlaygroundFile>;
@@ -20,14 +20,18 @@
rows?: number;
onOutput: (buffer: ArrayBuffer) => void;
}): Promise<BrowserPodTerminal>;
- createDirectory(path: string, opts?: {recursive?: boolean}): Promise<void>;
+ createDirectory(path: string, opts?: { recursive?: boolean }): Promise<void>;
createFile(path: string, mode: `binary` | `utf-8`): Promise<BrowserPodFile>;
- run(executable: string, args: Array<string>, opts: {
- cwd?: string;
- echo?: boolean;
- env?: Array<string>;
- terminal: BrowserPodTerminal;
- }): Promise<unknown>;
+ run(
+ executable: string,
+ args: Array<string>,
+ opts: {
+ cwd?: string;
+ echo?: boolean;
+ env?: Array<string>;
+ terminal: BrowserPodTerminal;
+ },
+ ): Promise<unknown>;
};
type BrowserPodFile = {
@@ -65,7 +69,12 @@
function getBrowserPodApiKey() {
const env = import.meta.env as Record<string, string | undefined>;
- return env.PUBLIC_BROWSERPOD_API_KEY ?? env.VITE_BPAPIKEY ?? env.VITE_BP_APIKEY ?? ``;
+ return (
+ env.PUBLIC_BROWSERPOD_API_KEY ??
+ env.VITE_BPAPIKEY ??
+ env.VITE_BP_APIKEY ??
+ ``
+ );
}
function dirname(path: string) {
@@ -74,11 +83,9 @@
}
function formatUnknownError(error: unknown) {
- if (error instanceof Error)
- return error.message;
+ if (error instanceof Error) return error.message;
- if (typeof error === `string`)
- return error;
+ if (typeof error === `string`) return error;
if (error === undefined)
return `BrowserPod rejected without an error message`;
@@ -90,11 +97,16 @@
}
}
-async function writeProjectFiles(pod: BrowserPodInstance, files: Array<PlaygroundFile>) {
+async function writeProjectFiles(
+ pod: BrowserPodInstance,
+ files: Array<PlaygroundFile>,
+) {
try {
- await pod.createDirectory(PROJECT_PATH, {recursive: true});
+ await pod.createDirectory(PROJECT_PATH, { recursive: true });
} catch (error) {
- throw new Error(`Failed to create ${PROJECT_PATH}: ${formatUnknownError(error)}`);
+ throw new Error(
+ `Failed to create ${PROJECT_PATH}: ${formatUnknownError(error)}`,
+ );
}
const directories = new Set<string>();
@@ -102,15 +114,14 @@
for (const file of files) {
const directory = dirname(file.path);
- if (directory)
- directories.add(directory);
+ if (directory) directories.add(directory);
}
for (const directory of directories) {
const path = `${PROJECT_PATH}/${directory}`;
try {
- await pod.createDirectory(path, {recursive: true});
+ await pod.createDirectory(path, { recursive: true });
} catch (error) {
throw new Error(`Failed to create ${path}: ${formatUnknownError(error)}`);
}
@@ -132,10 +143,9 @@
async function writeYarnBinary(pod: BrowserPodInstance) {
const response = await fetch(YARN_BIN_ASSET);
- if (!response.ok)
- return false;
+ if (!response.ok) return false;
- await pod.createDirectory(YARN_BIN_DIR, {recursive: true});
+ await pod.createDirectory(YARN_BIN_DIR, { recursive: true });
const podFile = await pod.createFile(YARN_BIN_PATH, `binary`);
await podFile.write(await response.arrayBuffer());
@@ -146,40 +156,41 @@
async function writeShellConfig(pod: BrowserPodInstance) {
const podFile = await pod.createFile(BASHRC_PATH, `utf-8`);
- await podFile.write([
- `export PATH="${YARN_BIN_DIR}:$PATH"`,
- `export npm_config_user_agent="yarn-playground"`,
- `export PS1="\\[\\e[38;2;134;239;172m\\]yarn-playground\\[\\e[0m\\] \\[\\e[38;2;148;163;184m\\]\\w\\[\\e[0m\\] $ "`,
- ``,
- `cd ${PROJECT_PATH}`,
- ``,
- ].join(`\n`));
+ await podFile.write(
+ [
+ `export PATH="${YARN_BIN_DIR}:$PATH"`,
+ `export npm_config_user_agent="yarn-playground"`,
+ `export PS1="\\[\\e[38;2;134;239;172m\\]yarn-playground\\[\\e[0m\\] \\[\\e[38;2;148;163;184m\\]\\w\\[\\e[0m\\] $ "`,
+ ``,
+ `cd ${PROJECT_PATH}`,
+ ``,
+ ].join(`\n`),
+ );
await podFile.close();
}
-export function PlaygroundTerminal({files, version}: Props) {
+export function PlaygroundTerminal({ files, version }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
- if (!container)
- return undefined;
+ if (!container) return undefined;
let disposed = false;
let term: XtermTerminal | null = null;
let browserPodTerminal: BrowserPodTerminal | null = null;
let resizeObserver: ResizeObserver | null = null;
let focusTerm: (() => void) | null = null;
+ let pod: BrowserPodInstance | null = null;
async function start() {
- const [{Terminal}, {FitAddon}] = await Promise.all([
+ const [{ Terminal }, { FitAddon }] = await Promise.all([
import(`@xterm/xterm`),
import(`@xterm/addon-fit`),
]);
- if (disposed)
- return;
+ if (disposed) return;
const fitAddon = new FitAddon();
term = new Terminal({
@@ -218,22 +229,20 @@
term.loadAddon(fitAddon);
term.open(container);
- term.onData(data => browserPodTerminal?.readData(data));
+ term.onData((data) => browserPodTerminal?.readData(data));
focusTerm = () => term?.focus();
container.addEventListener(`pointerdown`, focusTerm);
requestAnimationFrame(() => {
- if (!term || disposed)
- return;
+ if (!term || disposed) return;
fitAddon.fit();
term.focus();
});
resizeObserver = new ResizeObserver(() => {
- if (!term || disposed)
- return;
+ if (!term || disposed) return;
fitAddon.fit();
});
@@ -261,22 +270,21 @@
}
try {
- const {BrowserPod} = await import(/* @vite-ignore */ BROWSERPOD_RUNTIME_URL) as {BrowserPod: BrowserPodApi | null};
+ const { BrowserPod } = (await import(
+ /* @vite-ignore */ BROWSERPOD_RUNTIME_URL
+ )) as { BrowserPod: BrowserPodApi | null };
- if (!BrowserPod)
- throw new Error(`BrowserPod runtime failed to load`);
+ if (!BrowserPod) throw new Error(`BrowserPod runtime failed to load`);
- const pod = await BrowserPod.boot({apiKey});
+ pod = await BrowserPod.boot({ apiKey });
- if (disposed || !term)
- return;
+ if (disposed || !term) return;
browserPodTerminal = await pod.createCustomTerminal({
cols: term.cols,
rows: term.rows,
- onOutput: buffer => {
- if (!term || disposed)
- return;
+ onOutput: (buffer) => {
+ if (!term || disposed) return;
term.write(new Uint8Array(buffer));
},
@@ -284,6 +292,8 @@
await writeProjectFiles(pod, files);
+ if (disposed || !term) return;
+
if (await writeYarnBinary(pod)) {
writeLines(term, [
`${cyan}[browserpod]${reset} Mounted ${YARN_BIN_ASSET}`,
@@ -304,11 +314,13 @@
`${cyan}[browserpod]${reset} Opening BrowserPod bash in the mounted project instead.`,
]);
- await pod.run(`bash`, [], {cwd: PROJECT_PATH, terminal: browserPodTerminal});
+ await pod.run(`bash`, [], {
+ cwd: PROJECT_PATH,
+ terminal: browserPodTerminal,
+ });
}
} catch (error) {
- if (!term || disposed)
- return;
+ if (!term || disposed) return;
writeLines(term, [
`${red}[browserpod]${reset} ${formatUnknownError(error)}`,
@@ -321,12 +333,18 @@
return () => {
disposed = true;
+ pod = null;
+ browserPodTerminal = null;
resizeObserver?.disconnect();
- if (focusTerm)
- container.removeEventListener(`pointerdown`, focusTerm);
+ if (focusTerm) container.removeEventListener(`pointerdown`, focusTerm);
term?.dispose();
};
}, [files, version]);
- return <div ref={containerRef} className={`playground-terminal-mount absolute inset-[20px_22px] min-h-0 min-w-0 rounded-xl max-[560px]:inset-3.5`} />;
+ return (
+ <div
+ ref={containerRef}
+ className={`playground-terminal-mount absolute inset-[20px_22px] min-h-0 min-w-0 rounded-xl max-[560px]:inset-3.5`}
+ />
+ );
}You can send follow-ups to the cloud agent here.
| if (focusTerm) | ||
| container.removeEventListener(`pointerdown`, focusTerm); | ||
| term?.dispose(); | ||
| }; |
There was a problem hiding this comment.
BrowserPod sessions leak on remount
Medium Severity
The terminal effect calls BrowserPod.boot and pod.run but cleanup only disposes xterm. Changing presets or when files/version change retriggers the effect without stopping the prior pod or shell, so multiple BrowserPod instances and bash sessions can run and keep consuming memory and API quota.
Reviewed by Cursor Bugbot for commit 4d90be3. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: BrowserPod cfg wrong architecture
- Changed all cfg conditions from target_arch='wasm64' to target_arch='wasm32' to match the actual wasm32-browserpod-linux-musl build target.
Or push these changes by commenting:
@cursor push b55bf90779
Preview (b55bf90779)
diff --git a/packages/zpm-switch/Cargo.toml b/packages/zpm-switch/Cargo.toml
--- a/packages/zpm-switch/Cargo.toml
+++ b/packages/zpm-switch/Cargo.toml
@@ -35,10 +35,10 @@
[target.'cfg(unix)'.dependencies]
libc = "0.2"
-[target.'cfg(all(target_arch = "wasm64", target_vendor = "browserpod"))'.dependencies]
+[target.'cfg(all(target_arch = "wasm32", target_vendor = "browserpod"))'.dependencies]
reqwest = { workspace = true, default-features = false, features = ["default-tls"] }
-[target.'cfg(not(all(target_arch = "wasm64", target_vendor = "browserpod")))'.dependencies]
+[target.'cfg(not(all(target_arch = "wasm32", target_vendor = "browserpod")))'.dependencies]
reqwest = { workspace = true, default-features = false, features = ["rustls-tls"] }
[dev-dependencies]
diff --git a/packages/zpm-switch/src/http.rs b/packages/zpm-switch/src/http.rs
--- a/packages/zpm-switch/src/http.rs
+++ b/packages/zpm-switch/src/http.rs
@@ -8,7 +8,7 @@
static HTTP_CLIENT: LazyLock<Result<Client, Error>> = LazyLock::new(|| {
let mut builder = reqwest::Client::builder();
- #[cfg(not(all(target_arch = "wasm64", target_vendor = "browserpod")))]
+ #[cfg(not(all(target_arch = "wasm32", target_vendor = "browserpod")))]
{
builder = builder.use_rustls_tls();
}
diff --git a/packages/zpm-utils/src/system.rs b/packages/zpm-utils/src/system.rs
--- a/packages/zpm-utils/src/system.rs
+++ b/packages/zpm-utils/src/system.rs
@@ -24,7 +24,7 @@
#[cfg(target_arch = "x86")]
const ARCH: Cpu = Cpu::I386;
-#[cfg(target_arch = "wasm64")]
+#[cfg(target_arch = "wasm32")]
const ARCH: Cpu = Cpu::Wasm64;
#[cfg(target_os = "linux")]
diff --git a/packages/zpm/Cargo.toml b/packages/zpm/Cargo.toml
--- a/packages/zpm/Cargo.toml
+++ b/packages/zpm/Cargo.toml
@@ -75,10 +75,10 @@
[target.'cfg(unix)'.dependencies]
libc = "0.2"
-[target.'cfg(all(target_arch = "wasm64", target_vendor = "browserpod"))'.dependencies]
+[target.'cfg(all(target_arch = "wasm32", target_vendor = "browserpod"))'.dependencies]
reqwest = { workspace = true, default-features = false, features = ["default-tls"] }
-[target.'cfg(not(all(target_arch = "wasm64", target_vendor = "browserpod")))'.dependencies]
+[target.'cfg(not(all(target_arch = "wasm32", target_vendor = "browserpod")))'.dependencies]
reqwest = { workspace = true, default-features = false, features = ["rustls-tls"] }
[build-dependencies]
diff --git a/packages/zpm/src/http.rs b/packages/zpm/src/http.rs
--- a/packages/zpm/src/http.rs
+++ b/packages/zpm/src/http.rs
@@ -5,7 +5,7 @@
use hickory_resolver::{config::LookupIpStrategy, TokioResolver};
use http::HeaderMap;
use itertools::Itertools;
-#[cfg(not(all(target_arch = "wasm64", target_vendor = "browserpod")))]
+#[cfg(not(all(target_arch = "wasm32", target_vendor = "browserpod")))]
use reqwest::Identity;
use reqwest::{dns::{self, Addrs}, header::{HeaderName, HeaderValue}, Body, Certificate, Client, ClientBuilder, Method, Proxy, RequestBuilder, Response, Url};
use tokio::sync::OnceCell;
@@ -282,7 +282,7 @@
fn build_client(config: &Configuration, network_settings: Option<&NetworkSettings>) -> Result<Client, Error> {
let mut client_builder = reqwest::Client::builder();
- #[cfg(not(all(target_arch = "wasm64", target_vendor = "browserpod")))]
+ #[cfg(not(all(target_arch = "wasm32", target_vendor = "browserpod")))]
{
client_builder = client_builder.use_rustls_tls();
}
@@ -347,14 +347,14 @@
match (https_cert_file_path, https_key_file_path) {
(Some(cert_path), Some(key_path)) => {
- #[cfg(all(target_arch = "wasm64", target_vendor = "browserpod"))]
+ #[cfg(all(target_arch = "wasm32", target_vendor = "browserpod"))]
{
let _ = (cert_path, key_path);
return Err(Error::ConflictingOptions("httpsCertFilePath / httpsKeyFilePath (PEM client identity) require reqwest's rustls-tls feature, currently disabled for the browserpod target".to_string()));
}
- #[cfg(not(all(target_arch = "wasm64", target_vendor = "browserpod")))]
+ #[cfg(not(all(target_arch = "wasm32", target_vendor = "browserpod")))]
{
let cert_content
= cert_path.fs_read_prealloc()?;You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit ed94ea9. Configure here.
| reqwest = { workspace = true, default-features = false, features = ["default-tls"] } | ||
|
|
||
| [target.'cfg(not(all(target_arch = "wasm64", target_vendor = "browserpod")))'.dependencies] | ||
| reqwest = { workspace = true, default-features = false, features = ["rustls-tls"] } |
There was a problem hiding this comment.
BrowserPod cfg wrong architecture
High Severity
BrowserPod-specific TLS and CPU setup is gated on target_arch = "wasm64", but the playground WASM build and manifest use the triple wasm32-browserpod-linux-musl, where Rust exposes target_arch = "wasm32". The BrowserPod branches (default TLS, skipping rustls, ARCH) may never run on the actual build target.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit ed94ea9. Configure here.



Work in progress
Note
Medium Risk
Splits HTTP/TLS behavior by compile target (rustls vs native TLS) and adds a large new surface (BrowserPod API keys, COEP/COOP, WASM binary pipeline); native desktop builds should stay on rustls via cfg, but TLS and networking regressions on either target warrant careful review.
Overview
Adds a browser-based Yarn playground on
/playground/: preset sample projects (PnP, workspaces, node-modules), a read-only Monaco file tree, and an xterm terminal backed by BrowserPod that mounts template files and optionally a prebuiltyarn-bin.wasmso users can run Yarn in the remote shell.Rust / BrowserPod target:
zpmandzpm-switchgainwasm64+target_vendor = "browserpod"support—Cpu::Wasm64,rustc-check-cfgin build scripts, and target-specificreqwestTLS (default-tlsin the browser pod vsrustls-tlselsewhere). PEM client identity is rejected on the browserpod build. Workspacereqwest/dialoguerdefaults are trimmed; abuild:browserpod-yarnscript buildsyarn-binwith the BrowserPod Rust toolchain and publishes assets underwebsite/public/browserpod/.Site wiring: Playground nav link, COOP/COEP headers on
/playground*(middleware, Vite dev/preview plugin, static_headers) forSharedArrayBuffer, plus template loading fromwebsite/src/playground/templateswith{{YARN_VERSION}}substitution.Reviewed by Cursor Bugbot for commit ed94ea9. Bugbot is set up for automated code reviews on this repo. Configure here.