This guide is the fast path for working on the browser-side v2 bridge under v2/browser-bridge/.
public/v2/browser-bridge/For the current slice scope and artifacts, read these first:
docs/v2/browser-bridge/plan.mddocs/v2/browser-bridge/OPEN_CANVAS_API.mddocs/v2/browser-bridge/HOTLOAD_METHOD.mddocs/v2/browser-bridge/DEVTOOLS_DOM_MIRROR.mdInstall the shared v2 toolchain first:
docs/QUICKSTART.mdYou also need:
brew install cmake meson ninja python3 node git bash
xcode-select --install
sudo apt-get update
sudo apt-get install -y cmake meson ninja-build python3 python3-pip git build-essential curl
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
git clone https://github.com/emscripten-core/emsdk.git ~/emsdk
cd ~/emsdk && ./emsdk install 5.0.6 && ./emsdk activate 5.0.6
source ~/emsdk/emsdk_env.sh
npm ci
npx playwright install chromium
This is the preferred entry point for bridge work:
bash v2/browser-bridge/scripts/build.sh
It runs, in order:
wasm32, wasm32-simd, wasm64, wasm64-simd)public/v2/browser-bridge/The generated manifest is the single source of truth for browser runtime assets.
Downstream pages should consume that shared manifest instead of keeping their own
copied runtime/ trees, so a fresh bridge build immediately updates every v2
browser surface.
npm run lint:v2
npm run typecheck:v2
cd v2/browser-bridge
npm run typecheck
npm run build:v2:browser-bridge
npx playwright test -c v2/browser-bridge/playwright.config.ts
or:
npm run test:v2:browser-bridge
The bridge owns touch input on the canvas and sets touch-action: none. This
keeps EffinDom scrollbars, hit testing, overlays, and accessibility geometry in
the same coordinate system instead of letting browser page zoom scale the whole
document.
Framework-owned pinch zoom and control-owned two-finger gestures are planned as EffinDom gestures rather than browser visual viewport zoom.
The bridge page is a static site under public/v2/browser-bridge/. WASM
streaming and SharedArrayBuffer require proper HTTP headers, so it must be
served — opening index.html via file:// won’t work.
npx serve (quickest)npx serve public/v2/browser-bridge -p 4000 \
--set-headers "Cross-Origin-Opener-Policy: same-origin" \
--set-headers "Cross-Origin-Embedder-Policy: require-corp"
Then open http://localhost:4000 in your browser of choice.
Tier 2 copy now exits wasm as a structured clipboard payload instead of a plain string-only callback.
plainText is always present and remains the fallback path.RichText selections can also include bridge-generated text/html.web application/x-effindom-richtext+json.The browser bridge owns the actual navigator.clipboard.write(...) call. If
the browser rejects rich clipboard writes, it falls back to
navigator.clipboard.writeText(plainText) so plain copy still succeeds.
Use the DevTools DOM Mirror when you need to inspect
the retained EffinDom tree in browser DevTools. Debug builds default the mirror
to on-requested, release builds default it to disabled, and apps can override
window.__effindomRuntime.devToolsDomMirror or the harness option explicitly.
Press Shift+Meta+F12 to open the debug dialog when the mode is enabled or
on-requested. The dialog can enable the mirror and toggle Inspect Mode.
python3 -c "
import http.server, socketserver
class Handler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
super().end_headers()
with socketserver.TCPServer(('', 4000), Handler) as httpd:
print('Serving at http://localhost:4000')
httpd.serve_forever()
" --directory public/v2/browser-bridge
Open the browser console. The bridge logs the selected renderer on boot:
[EffinDom] renderer: webgpu ← best case
[EffinDom] renderer: webgl2 ← WebGPU unavailable (console.warn)
[EffinDom] renderer: cpu ← GPU entirely unavailable (console.error)
window.__bridgeLoaderInfo.activeRenderer also holds the string at runtime.
There is no URL parameter to force a backend at runtime. To test a specific path, disable the higher-tier backends at the browser level:
| Target backend | How to reach it |
|---|---|
| WebGL2 | Disable WebGPU: launch Chromium with --disable-webgpu |
| CPU | Disable both: --disable-webgpu --disable-webgl |
Alternatively, the Playwright tests stub navigator.gpu and
canvas.getContext('webgl2') at the JS level — see smoke.spec.ts for the
exact patch approach.
The full build pipeline (bash v2/browser-bridge/scripts/build.sh) is slow
because it rebuilds WASM. For iterating on TypeScript alone:
npx esbuild v2/browser-bridge/src/bridge.ts \
--bundle --format=iife --platform=browser --target=es2020 \
--outfile=public/v2/browser-bridge/bridge.js --sourcemap
Then hard-refresh the browser page (Ctrl+Shift+R / Cmd+Shift+R).
Rendering backend, C ABI, and staged runtime asset changes should use the repo-root build path instead:
./build.sh
That path refreshes the Core/WASM outputs and the staged browser assets used by the public smoke pages. For targeted browser runtime iteration, run:
npm run build:v2:browser-bridge
Run npm run test:v2:browser after ABI or staged asset changes. The standalone
Core smoke page also has staged command-buffer payloads, so update it whenever a
draw command ABI changes.
The browser bridge exposes its runtime at:
const runtime = await window.EffinDomBrowserBridge!.ready;
await runtime.registerFont({
id: 100,
url: '/fonts/Inter-Regular.ttf',
});
If you want the same lazy behavior used by the FUI-AS browser harness, register the source first and fetch it only on first use:
runtime.registerLazyFont(100, '/fonts/Inter-Regular.ttf');
await runtime.ensureFont(100);
If you already have fallback faces loaded and want to attach them to that primary face:
await runtime.registerFont({
id: 100,
url: '/fonts/Inter-Regular.ttf',
fallbackIds: [101, 102],
});
await runtime.registerFontStack({
primary: {
id: 100,
url: '/fonts/Inter-Regular.ttf',
},
fallbacks: [
{
id: 101,
url: '/fonts/NotoColorEmoji.ttf',
},
{
id: 102,
url: '/fonts/NotoSansSymbols2-Regular.ttf',
},
],
});
Then point your text node at the primary face:
runtime.ui._ui_set_font(textHandle, 100, 24);
runtime.commitFrame();
Rules:
id values must be unique within the runtime session.url values must be fetchable by the page.registerLazyFont(...) records a custom font source without fetching it yet; ensureFont(...) performs the first load on demand.registerFontStack(...) loads every face first, then attaches the fallback chain to the primary face.The browser bridge bundles a built-in monospace pair under IDs 5 (regular) and
6 (bold), but it does not fetch them during startup. Load only the face
you need:
await runtime.ensureBuiltInFont(5);
runtime.ui._ui_set_font(textHandle, 5, 15);
runtime.commitFrame();
ensureBuiltInFont(...) only works for bundled built-in font IDs.public/v2/browser-bridge/index.htmlpublic/v2/browser-bridge/{bridge,harness}.jspublic/v2/browser-bridge/effindom.v2.manifest.jsonpublic/v2/browser-bridge/runtime/v2/browser-bridge/tests/screenshots/@effindomv2/runtime)The publishable runtime package now stages browser assets under
v2/browser-bridge/dist/:
dist/bridge.js + dist/harness.jsdist/effindom.v2.manifest.jsondist/runtime/**dist/fonts/** (Noto baseline set, excluding Thai/Arabic files)Use:
cd v2/browser-bridge
npm run build:package:assets
The package also exports runtime URL/config helpers from
@effindomv2/runtime/runtime-config (and the root barrel) so framework packages
and end-user apps can resolve the packaged manifest URL without relying on
repo-private public/v2/** paths.
The current in-page route-swap method for routed shells such as
v2/fui-as/demo/** is
documented in:
docs/v2/browser-bridge/HOTLOAD_METHOD.mdThat doc covers the live state split, what stays hot, what gets recreated on each route swap, the bridge/harness entry points involved, and the guardrails that keep warm reboots safe.