The desktop tool had nodeIntegration: true since day one because it was faster to build that
way. It also meant the renderer could call fs directly, which meant every feature had grown
its own ad-hoc file access scattered across components.
Today I turned it off.
What it took
Every Node call in the renderer had to move behind contextBridge. I expected this to be
tedious and it was, but the surprise was how much code just evaporated. Four different
components had each written their own “read the project JSON, parse it, handle the missing
file case” logic. Now there’s one:
// preload.js
contextBridge.exposeInMainWorld('api', {
readProject: (id) => ipcRenderer.invoke('project:read', id),
writeProject: (id, data) => ipcRenderer.invoke('project:write', id, data),
listProjects: () => ipcRenderer.invoke('project:list'),
});
Net: -412 lines, and the renderer no longer knows what a file path is.
What broke
The file watcher. It was using fs.watch in the renderer to refresh the project list, and
there’s no clean way to push events across the bridge without setting up the reverse channel.
Ended up with ipcRenderer.on('projects:changed', ...) and a webContents.send from main.
Worth noting for later: the watcher fires twice on Windows for a single save. Debounced it at 120ms, which is a bandaid, not a fix.
Tomorrow
Same treatment for the ffmpeg calls. Those are worse — they stream progress, so it’s not a simple request/response.