Skip to content

Instantly share code, notes, and snippets.

@3p3r
Created March 19, 2026 01:11
Show Gist options
  • Select an option

  • Save 3p3r/6c523f1783f84e034d6c832ab4216427 to your computer and use it in GitHub Desktop.

Select an option

Save 3p3r/6c523f1783f84e034d6c832ab4216427 to your computer and use it in GitHub Desktop.
Electron shim for web builds
import { directoryOpen, fileOpen } from 'browser-fs-access';
import debug from 'debug';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import { memoize } from 'lodash';
import { bus } from './bus';
import { MemoryInitializer } from './memory';
const log = debug('pleadable:web_ipc');
export default {};
/** Options for showOpenDialog (web: uses browser-fs-access) */
interface OpenDialogOptions {
properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles'>;
}
export const ipcRenderer = new (class extends EventEmitter {
send(channel: string, ...args: unknown[]): void {
log('ipcRenderer.send channel: %s %o', channel, args);
ipcMain.emit(channel, {}, ...args);
}
async invoke(channel: string, ...args: unknown[]): Promise<unknown> {
log('ipcRenderer.invoke channel: %s %o', channel, args);
const output = await ipcMain.emitAsync(channel, ...args);
if (channel === 'auth:getToken') {
log('ipcRenderer.invoke auth:getToken result (token): %s', output ?? '(empty)');
}
MemoryInitializer.persist(); // persist in background
return output;
}
})();
export const contextBridge = {
exposeInMainWorld: (key: string, api: unknown): void => {
if (typeof window !== 'undefined') {
(window as unknown as Record<string, unknown>)[key] = api;
}
if (typeof globalThis !== 'undefined') {
(globalThis as unknown as Record<string, unknown>)[key] = api;
}
},
};
const BrowserWindow = class extends EventEmitter {
private static instance: InstanceType<typeof BrowserWindow>;
constructor() {
super();
BrowserWindow.instance = this;
}
static getAllWindows(): InstanceType<typeof BrowserWindow>[] {
return BrowserWindow.instance ? [BrowserWindow.instance] : [];
}
static fromWebContents(_webContents: unknown): InstanceType<typeof BrowserWindow> | null {
return BrowserWindow.instance || null;
}
webContents = new (class {
setWindowOpenHandler(
handler: (details: { url: string }) => { action: 'allow' | 'deny' },
): void {
log('setWindowOpenHandler: %o', handler);
}
send(channel: string, ...args: unknown[]): void {
log('webContents.send channel: %s %o', channel, args);
// Emit with null as first argument for IPC event shape
ipcRenderer.emit(channel, null, ...args);
}
})();
loadFile(filePath: string): void {
log('BrowserWindow.loadFile: %s', filePath);
}
};
export { BrowserWindow };
export const shell = {
openExternal: async (url: string): Promise<void> => {
log('shell.openExternal: %s', url);
},
};
export const nativeImage = {};
export const ipcMain = new (class extends EventEmitter {
private handlers = new Map<string, (event: null, ...args: unknown[]) => Promise<unknown>>();
handle(channel: string, listener: (event: null, ...args: unknown[]) => Promise<unknown>): void {
log('ipcMain.handle channel: %s', channel);
this.handlers.set(channel, listener);
}
async emitAsync(channel: string, ...args: unknown[]): Promise<unknown> {
const handler = this.handlers.get(channel);
if (handler) {
const output = await handler(null, ...args);
MemoryInitializer.persist(); // persist in background
return output;
}
return undefined;
}
})();
export const dialog = {
showOpenDialog: async (
options?: OpenDialogOptions,
): Promise<{ canceled: boolean; filePaths: string[] }> => {
log('dialog.showOpenDialog options: %o', options);
try {
const properties = options?.properties || [];
if (properties.includes('openDirectory')) {
const files = await directoryOpen({ recursive: true });
const fileList = Array.isArray(files) ? files : [files];
if (fileList.length === 0) {
return { canceled: false, filePaths: ['/'] };
}
const basePath = `/${fileList[0].webkitRelativePath.split(/[/\\]/)[0]}`;
for (const file of fileList) {
const targetPath = `/${file.webkitRelativePath.replace(/\\/g, '/')}`;
await writeFileToFs(file, targetPath);
}
return { canceled: false, filePaths: [basePath] };
} else {
const multiple = properties.includes('multiSelections');
const files = await fileOpen({ multiple });
const fileList = files == null ? [] : Array.isArray(files) ? files : [files];
const filePaths: string[] = [];
for (const file of fileList) {
const filePath = `/${file.name}`;
await writeFileToFs(file, filePath);
filePaths.push(filePath);
}
return { canceled: false, filePaths };
}
} catch (error) {
log('Dialog canceled or error: %o', error);
return { canceled: true, filePaths: [] };
}
},
};
/**
* Write a File from browser-fs-access to the virtual filesystem
*/
async function writeFileToFs(file: File, targetPath: string): Promise<void> {
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const parentDir = targetPath.substring(0, targetPath.lastIndexOf('/'));
if (parentDir) {
fs.mkdirSync(parentDir, { recursive: true });
}
fs.writeFileSync(targetPath, uint8Array);
log('Synced file to workspace: %s', targetPath);
}
export const app = new (class extends EventEmitter {
private _whenReady: () => Promise<void>;
constructor() {
super();
this._whenReady = memoize(async () => {
await new Promise((resolve) => {
bus.once('appReady', resolve);
});
});
}
whenReady = async () => {
await this._whenReady();
};
getPath = (name: string) => {
// Return paths for web app (posix-style)
const paths: Record<string, string> = {
userData: '/tmp/openwork-userdata',
appData: '/tmp/openwork-appdata',
temp: '/tmp',
home: '/home',
};
const path = paths[name] || '/tmp';
// Ensure the directory exists in the workspace
if (!fs.existsSync(path)) {
try {
fs.mkdirSync(path, { recursive: true });
} catch (error) {
log('Failed to create directory %s: %o', path, error);
}
}
return path;
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment