@divriots/h2d-electron-sdk
v0.0.4
Published
Capture an Electron app's renderer into an html.to.design payload. Dev builds only.
Keywords
Readme
@divriots/h2d-electron-sdk
Capture any Electron app's renderer into an html.to.design
.h2d payload. Add one line to your main process, trigger a capture from
your own UI, and save the bytes (or convert them with the ‹div›RIOTS
public API in your own code).
Dev builds only.
mountCaptureno-ops in packaged production builds unless you opt in.
Install
pnpm add -D @divriots/h2d-electron-sdkelectron is a peer dependency (any version ≥ 22).
Usage
// In your Electron main process
import { app, BrowserWindow, dialog } from 'electron';
import { writeFile } from 'fs/promises';
import { mountCapture, captureWebContents } from '@divriots/h2d-electron-sdk';
// Call once, after `app` is available — registers the capture preload in
// the default session. No UI is added; you wire your own trigger.
mountCapture(app);
// From your own menu item / shortcut / button:
async function captureToFile(win: BrowserWindow): Promise<void> {
const { bytes, filename } = await captureWebContents(win.webContents);
const { filePath } = await dialog.showSaveDialog(win, {
defaultPath: `${filename}.h2d`,
});
if (filePath) await writeFile(filePath, Buffer.from(bytes));
}Drop the resulting .h2d file into the html.to.design Figma plugin — or
POST the bytes to the ‹div›RIOTS public API yourself for a
Figma-pasteable (figma-clipboard) conversion.
API
mountCapture(app, options?)
Registers the renderer preload in the default session. Call once, after
app is available.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| enabledIn | (app) => boolean | dev only | Opt capture into packaged production builds. By default mountCapture no-ops when app.isPackaged is true. |
captureWebContents(webContents, options?)
Captures a webContents and resolves with the .h2d payload.
mountCapture must have been called first.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| url | string | wc.getURL() | Override the URL baked into the capture. |
Returns:
| Field | Type | Description |
| --- | --- | --- |
| bytes | Uint8Array | The .h2d payload. |
| filename | string | Suggested filename (no extension). |
Renderer requirements
None — the SDK works with Electron's default webPreferences. The
preload is bundled sandbox-compatible (its only external is
require('electron')), so sandbox: true and contextIsolation: true
are both fine.
Security model
The capture engine (the proprietary dom-serializer) runs entirely in the
renderer's sandboxed isolated world — the same context the preload
already lives in. It has no Node, no fs/net, and no access to your
app beyond the page it's capturing. It reaches the main process only
through a small, enumerable set of IPC channels for the three main-only
Electron capabilities it needs:
- CDP (the debugger protocol) —
h2d:cdp/h2d:cdp-event - cross-origin iframe execution (
WebFrameMain) —h2d:iframe - frame enumeration —
h2d:frames
The main-process side of the SDK (dist/index.js) is shipped
un-obfuscated — it's a thin proxy that forwards those calls and adds no
IP. You can read 100% of what runs in your main process. The capture is
fully local; the SDK makes no network calls (conversion to Figma is
left to your own code via the public API).
Limitations
- Dev only by default.
mountCaptureno-ops in packaged builds unless you setenabledIn. - Same-origin iframes are captured inline. Cross-origin iframes
are captured too, but lossy on a few details (platform-font specifics
fall back to
getComputedStyle's family; forced pseudo-states are approximated). A cross-origin iframe nested inside another cross-origin iframe is dropped at the second level. - Open shadow roots only. Closed shadow DOM is captured as opaque.
Troubleshooting
TypeError: Cannot read properties of undefined (reading 'on') —
your shell has ELECTRON_RUN_AS_NODE=1 set, which makes the Electron
binary run as plain Node with no Electron APIs. Unset it:
unset ELECTRON_RUN_AS_NODE