lunx.docs
DocsFrameworksElectron

Electron Adapter

Build cross-platform desktop apps with Node.js as the main process and any web framework as the renderer. Lunx produces a dual bundle: CJS for main, ESM for the browser renderer.

Electron 31 · Dual Bundle · IPC
Dual bundle output

Lunx produces separate bundles: CJS Node.js main process + ESM browser renderer process.

IPC communication

contextBridge + ipcRenderer/ipcMain for safe, typed communication between processes.

BrowserWindow API

Create native OS windows, menus, dialogs, and system tray icons from the main process.

Native file system

Full Node.js fs access in main process. Renderer requests file ops via IPC channels.

Fast rebuilds

Lunx only recompiles changed modules. Renderer changes reload without restarting the main process.

Auto-updater ready

Works with electron-updater for seamless auto-update delivery via GitHub Releases or S3.

Setup

bash
npm create lunx-dev@latest my-electron-app -- --framework electron --ts
cd my-electron-app && npm install && npm run dev

Project structure

text
my-electron-app/
├── src/
   ├── main/
      ├── main.ts          # Electron main process entry
      ├── preload.ts       # contextBridge preload script
      └── ipc-handlers.ts  # IPC channel handlers
   └── renderer/            # React/Vue/Svelte frontend
       ├── main.tsx
       ├── App.tsx
       └── index.html
├── lunx.config.ts
└── package.json

Main process

typescriptsrc/main/main.ts
import { app, BrowserWindow, ipcMain, dialog } from 'electron'
import path from 'path'
import fs   from 'fs/promises'

let mainWindow: BrowserWindow | null = null

function createWindow() {
  mainWindow = new BrowserWindow({
    width:  1280,
    height: 800,
    webPreferences: {
      preload:           path.join(__dirname, 'preload.js'),
      contextIsolation:  true,   // required for security
      nodeIntegration:   false,  // never enable in renderer
    },
  })

  // Dev: load from Lunx dev server
  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:5173')
    mainWindow.webContents.openDevTools()
  } else {
    mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
  }
}

// IPC handler — runs in privileged main process
ipcMain.handle('dialog:openFile', async () => {
  const { filePaths } = await dialog.showOpenDialog({ properties: ['openFile'] })
  if (filePaths.length === 0) return null
  const content = await fs.readFile(filePaths[0], 'utf-8')
  return { path: filePaths[0], content }
})

app.whenReady().then(createWindow)

Preload — contextBridge

typescriptsrc/main/preload.ts
import { contextBridge, ipcRenderer } from 'electron'

// Expose a typed API to the renderer — no nodeIntegration needed
contextBridge.exposeInMainWorld('electronAPI', {
  openFile:   () => ipcRenderer.invoke('dialog:openFile'),
  saveFile:   (path: string, content: string) =>
    ipcRenderer.invoke('fs:writeFile', path, content),
  onMenuOpen: (callback: () => void) =>
    ipcRenderer.on('menu:open', callback),
})

Using the API in the renderer

typescriptsrc/renderer/App.tsx
// TypeScript types for contextBridge API
declare global {
  interface Window {
    electronAPI: {
      openFile:   () => Promise<{ path: string; content: string } | null>
      saveFile:   (path: string, content: string) => Promise<void>
      onMenuOpen: (callback: () => void) => void
    }
  }
}

export function FileEditor() {
  const [content, setContent] = useState('')
  const [filePath, setFilePath] = useState<string | null>(null)

  async function open() {
    const result = await window.electronAPI.openFile()
    if (result) {
      setContent(result.content)
      setFilePath(result.path)
    }
  }

  async function save() {
    if (filePath) await window.electronAPI.saveFile(filePath, content)
  }

  return (
    <div>
      <div>
        <button onClick={open}>Open File</button>
        <button onClick={save} disabled={!filePath}>Save</button>
        {filePath && <span>{filePath}</span>}
      </div>
      <textarea value={content} onChange={e => setContent(e.target.value)} />
    </div>
  )
}
Security: never enable nodeIntegration
Setting nodeIntegration: true in Electron gives every loaded web page direct access to Node.js APIs — a critical security vulnerability. Always use contextIsolation: true and the contextBridge preload pattern to expose only the specific APIs your renderer needs.