Electron IPC Code Generator
Generate type-safe Electron IPC APIs from TypeScript contracts.
Overview
@number10/electron-ipc is a publishable npm package for Electron applications. It includes a TypeScript code generator, contract helper types, generated preload API support, main-process handler helpers, and optional runtime modules for validation, streams, React hooks, multi-window routing, and IPC inspection.
The package removes repetitive IPC wiring and keeps the boundary between main, preload, and renderer code type-safe.
Why a generator? The generated APIs are still plain TypeScript, so you keep IDE support and refactoring safety while avoiding manual IPC boilerplate.
Key benefits at a glance:
- Refactor-friendly: rename or reshape a contract and TypeScript highlights every affected call site.
- Single source of truth: one contract definition drives main, preload, and renderer types.
- IDE-native: autocomplete and jump-to-definition work without custom tooling.
- Less boilerplate: generated APIs and handlers replace repetitive IPC glue.
The Key Benefit: When you change an IPC contract interface, TypeScript immediately shows compile errors everywhere the contract is used incorrectly - before you even run the code. No runtime surprises!
What Ships in the Package
- Generator CLI -
electron-ipc-generatereads your contract file and YAML config. - Contract types - helper types for invoke, events, broadcasts, renderer-to-renderer calls, and streams.
- Generated preload APIs - typed methods exposed through Electron's
contextBridge. - Main helpers - typed handler and event registration utilities.
- Optional modules - validation adapters, middleware, window registry, renderer routing, React hooks, and IPC Inspector.
The repository contains apps and templates for development and examples. They are not required to use the package.
Architecture
Communication Flow
The following diagram shows how the core IPC contract types work together in an Electron application:
packages/electron-ipc- Main library with code generator and runtime helperspackages/template-basic- Self-generating template demonstrating best practices
How It Works
- Define IPC Contracts in your main process using TypeScript interfaces
- Run the Generator via YAML configuration to create type-safe API code
- Use Generated API in preload/renderer with full TypeScript support
- Abstract Register Handler in main/index for secure registration of the Invoke/Events/Broadcast interfaces
Additional Modules
The generator ships with optional runtime modules:
- Window Management: Central registry + multi-window broadcast helpers. See Window Management.
- IPC Inspector: Dev-only IPC tracing UI with payload previews and export. See IPC Inspector.
IPC Contract Types
The generator supports five types of IPC communication:
1. Invoke (Renderer ↔ Main, Request/Response)
Renderer calls main process and waits for response:
export type InvokeContracts = GenericInvokeContract<{
GetVersion: IInvokeContract<void, string>
AddNumbers: IInvokeContract<{ a: number; b: number }, number>
}>Why these wrapper types? GenericInvokeContract and IInvokeContract enforce a strict structure with request and response properties. This structured format is required so the generator can reliably parse and generate type-safe code. You cannot deviate from this pattern - it ensures the generator knows exactly what types to extract.
Generated method names: The generator prefixes invoke methods with invoke:
AddNumbers→window.api.invokeAddNumbers(params)GetVersion→window.api.invokeGetVersion()
2. Event (Renderer → Main)
Renderer sends events to main process without expecting a response:
export type EventContracts = GenericRendererEventContract<{
Quit: IRendererEventContract<void>
LogMessage: IRendererEventContract<string>
}>Why these wrapper types? GenericRendererEventContract and IRendererEventContract enforce the structure with a request property. This is necessary for the generator to extract the payload type correctly.
Generated method names: The generator prefixes event methods with send:
Quit→window.api.sendQuit()LogMessage→window.api.sendLogMessage(message)
3. Broadcast (Main → Renderer)
Main process sends data/events to renderer (one-way only):
export type BroadcastContracts = GenericBroadcastContract<{
Ping: IBroadcastContract<number>
About: IBroadcastContract<void>
}>Why these wrapper types? GenericBroadcastContract and IBroadcastContract enforce the structure with a payload property. This structured format is required so the generator can reliably parse and generate type-safe code, just like the other contract types.
Generated method names: The generator prefixes broadcast listeners with on:
Ping→window.api.onPing((count) => ...)About→window.api.onAbout(() => ...)
4. Renderer Invoke (Renderer ↔ Renderer via Main)
Type-safe request-response communication between renderer processes, routed through main process for security:
export type RendererInvokeContracts = GenericRendererInvokeContract<{
GetDashboardData: IRendererInvokeContract<{ query: string }, { data: unknown[]; total: number }>
UpdateSettings: IRendererInvokeContract<{ theme: 'light' | 'dark' }, { success: boolean }>
}>Why these wrapper types? GenericRendererInvokeContract and IRendererInvokeContract enforce the structure with request and response properties, ensuring the generator can extract types correctly for both the caller and handler sides.
Generated method names:
- Caller:
window.api.rendererInvokeGetDashboardData('targetRole', request)→ sends to target window - Handler:
window.api.handleGetDashboardData((request, context) => { ... })→ registers handler in target window
Requirements:
- Initialize router in main:
initRendererInvokeRouter() - Register windows with roles:
getWindowRegistry().register(window, 'dashboard')
See Renderer-to-Renderer IPC for detailed guide.
5. Streams (Large Data & Real-time)
For efficient handling of large data transfers or real-time data streams using Web Streams API:
// Stream Invoke: Request-response with streaming response
export type StreamInvokeContracts = GenericStreamInvokeContract<{
GetLargeData: IStreamInvokeContract<{ offset: number }, string>
}>
// Stream Upload: Renderer uploads data to main
export type StreamUploadContracts = GenericStreamUploadContract<{
UploadFile: IStreamUploadContract<{ filename: string }, Uint8Array>
}>
// Stream Download: Main streams data to renderer
export type StreamDownloadContracts = GenericStreamDownloadContract<{
DownloadLogs: IStreamDownloadContract<{ sinceMs: number }, string>
}>Generated method names:
- Stream Invoke:
invokeStreamGetLargeData(request, callbacks)→ starts stream, returns cleanup - Stream Upload:
uploadUploadFile(request)→ returns StreamWriter - Stream Download:
downloadDownloadLogs(request, onData, onEnd?, onError?)→ starts stream, returns cleanup
IPC Patterns Quick Map
Use this as a fast mental model for which pattern to pick:
| Pattern | Direction | When to use |
|---|---|---|
| Invoke | Renderer ↔ Main | Request/response with a single result |
| Event | Renderer → Main | Fire-and-forget commands |
| Broadcast | Main → Renderer | One-way notifications to renderer(s) |
| Renderer Invoke | Renderer ↔ Renderer (via Main) | Multi-window request/response |
| Streams | Renderer ↔ Main | Large data or continuous updates |
First IPC in 5 Minutes (Checklist)
- Define contracts in
src/main/ipc-api.ts - Create config in
ipc-config.yaml - Generate APIs with
electron-ipc-generate --config=./ipc-config.yaml - Expose in preload with
exposeMyApi()(generated) - Call from renderer with
window.myApi.invokeXxx(...) - Register handlers in main via
AbstractRegisterHandler(ordefineInvokeHandlers)
Workflow
1. Define Contracts
Create IPC contracts in your main process (src/main/ipc-api.ts):
import {
createBroadcast,
GenericBroadcastContract,
GenericInvokeContract,
GenericRendererEventContract,
GenericStreamInvokeContract,
GenericStreamUploadContract,
GenericStreamDownloadContract,
IBroadcastContract,
IInvokeContract,
IRendererEventContract,
} from '@number10/electron-ipc'
// Invoke: Renderer calls main and gets response
export type InvokeContracts = GenericInvokeContract<{
AddNumbers: IInvokeContract<{ a: number; b: number }, number>
GetVersion: IInvokeContract<void, string>
}>
// Event: Renderer sends events to main
export type EventContracts = GenericRendererEventContract<{
Quit: IRendererEventContract<void>
LogMessage: IRendererEventContract<string>
}>
// Broadcast: Main sends to renderer (one-way)
export type BroadcastContracts = GenericBroadcastContract<{
Ping: IBroadcastContract<number>
About: IBroadcastContract<void>
}>
// Renderer Invoke: Renderer-to-renderer via main routing (multi-window apps)
export type RendererInvokeContracts = GenericRendererInvokeContract<{
GetDashboardData: IRendererInvokeContract<{ query: string }, { data: unknown[]; total: number }>
}>
// Streams: Large data and real-time communication
export type StreamInvokeContracts = GenericStreamInvokeContract<{
GetLargeData: IStreamInvokeContract<{ offset: number }, string>
}>
export type StreamUploadContracts = GenericStreamUploadContract<{
UploadFile: IStreamUploadContract<{ filename: string }, Uint8Array>
}>
export type StreamDownloadContracts = GenericStreamDownloadContract<{
DownloadLogs: IStreamDownloadContract<{ sinceMs: number }, string>
}>
// Optional: Create runtime broadcast helper (if not using generated API)
// export const mainBroadcast = createBroadcast<BroadcastContracts>()No Separate DSL
The generator does not require a custom scripting language. Your contracts are plain TypeScript types, and the generator reads them directly using ts-morph. The only "config" is a small YAML file that wires inputs and outputs; the actual API surface stays in TypeScript.
Why this matters:
- Refactor-friendly: rename or reshape a contract and TypeScript highlights every affected call site immediately.
- Single source of truth: the same types are used by main, preload, and renderer.
- IDE-native: autocomplete, jump-to-definition, and linting work without any extra tooling.
Configuration File
For managing IPC APIs, use a YAML configuration file:
# ipc-config.yaml
apis:
- name: api
input: ./src/main/ipc-api.ts
output: ./src/preload/api-generated.ts
tsconfig: ./tsconfig.json
contracts:
invoke: InvokeContracts
event: EventContracts
send: BroadcastContracts
rendererInvoke: RendererInvokeContracts
mainBroadcastOutput: ./src/main/broadcast-generated.ts
- name: streamApi
input: ./src/main/ipc-api-stream.ts
output: ./src/preload/api-stream-generated.ts
contracts:
streamInvoke: StreamInvokeContracts
streamUpload: StreamUploadContracts
streamDownload: StreamDownloadContractsDiagram: how the config maps to generated outputs:
Generate all APIs:
electron-ipc-generate --config=./ipc-config.yaml
# Watch mode (auto-regenerate on changes)
electron-ipc-generate --config=./ipc-config.yaml --watch
# CI mode (verify outputs without writing)
electron-ipc-generate --config=./ipc-config.yaml --checkEach API gets a unique expose function name (e.g., exposeApi, exposeStreamApi).
3. Use in Preload
Expose the generated API via context bridge (src/preload/index.ts):
import { exposeMyApi, MyApiType } from './api-generated'
declare global {
interface Window {
myApi: MyApiType
}
}
exposeMyApi()Alternative: You can also use the contextBridge directly:
import { contextBridge } from 'electron'
import { myApi } from './api-generated'
contextBridge.exposeInMainWorld('myApi', myApi)4. Use in Renderer
Access the API with full TypeScript support (src/renderer/App.tsx):
// Invoke: Call main and await response (prefixed with 'invoke')
const result = await window.myApi.invokeAddNumbers({ a: 5, b: 3 }) // result = 8
const version = await window.myApi.invokeGetVersion()
// Event: Send to main (prefixed with 'send')
window.myApi.sendQuit()
window.myApi.sendLogMessage('User clicked button')
// Broadcast: Listen to events from main (prefixed with 'on')
window.myApi.onPing((count) => console.log(`Ping ${count}`))
window.myApi.onAbout(() => console.log('About dialog'))
// Streams: Handle large data and real-time communication
const stopStream = window.myApi.invokeStreamGetLargeData(
{ offset: 0 },
{
onData: (chunk) => console.log('Received:', chunk),
onEnd: () => console.log('Stream complete'),
onError: (err) => console.error(err),
}
)
const uploadStream = window.myApi.uploadUploadFile({ filename: 'data.txt' })
await uploadStream.write(new Uint8Array([1, 2, 3, 4, 5]))
await uploadStream.close()
const stopDownload = window.myApi.downloadDownloadLogs(
{ sinceMs: Date.now() },
(log) => console.log('Log:', log),
() => console.log('Download complete'),
(err) => console.error(err)
)
// Note: Date is not serializable; use timestamps or ISO strings instead.
// Optional: stop stream early
// stopStream()
// stopDownload()React Hooks (Optional)
If you're using React, enable automatic hook generation in your configuration:
apis:
- name: api
# ... other config
reactHooksOutput: ./src/renderer/hooks/api-hooks.tsThis generates type-safe React hooks for all contract types:
useInvokeContracts - For invoke operations with automatic state management:
import { useInvokeContracts } from './hooks/api-hooks'
function MyComponent() {
const { data, loading, error, invoke } = useInvokeContracts('AddNumbers')
const handleClick = async () => {
try {
const result = await invoke({ a: 5, b: 3 })
console.log('Result:', result) // 8
} catch (err) {
console.error('Error:', err)
}
}
return (
<div>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <p>Result: {data}</p>}
<button onClick={handleClick}>Add Numbers</button>
</div>
)
}useEventContracts - For sending events to main:
import { useEventContracts } from './hooks/api-hooks'
function LogComponent() {
const { send } = useEventContracts('LogMessage')
return <button onClick={() => send('User clicked button')}>Log Message</button>
}useBroadcastContracts - For listening to broadcasts with automatic cleanup:
import { useBroadcastContracts } from './hooks/api-hooks'
import { useEffect } from 'react'
function PingDisplay() {
const { data, subscribe, unsubscribe } = useBroadcastContracts('Ping')
useEffect(() => {
const unsubscribeFn = subscribe((count) => {
console.log('Received ping:', count)
})
return unsubscribeFn
}, [subscribe])
return <div>Ping count: {data}</div>
}useStreamInvokeContracts, useStreamUploadContracts, useStreamDownloadContracts - For streaming operations:
import { useStreamDownloadContracts } from './hooks/api-hooks'
function LogViewer() {
const { data, loading, error, startStream, stopStream } = useStreamDownloadContracts('DownloadLogs')
const handleStart = () => {
startStream({ sinceMs: Date.now() - 3600000 }) // Last hour
}
return (
<div>
<button onClick={handleStart} disabled={loading}>
Start Stream
</button>
<button onClick={stopStream} disabled={!loading}>
Stop Stream
</button>
{data.map((log, i) => (
<div key={i}>{log}</div>
))}
</div>
)
}Note: The generated broadcast API (mainBroadcastOutput) is optional but recommended for consistency with the renderer API. Both approaches are type-safe.
5. Implement Handlers in Main
Handle IPC calls in main process (src/main/index.ts):
import {
AbstractRegisterHandler,
AbstractRegisterEvent,
AbstractRegisterStreamHandler,
AbstractRegisterStreamUpload,
AbstractRegisterStreamDownload,
IPCHandlerType,
IPCEventType,
} from '@number10/electron-ipc'
import { InvokeContracts, EventContracts, BroadcastContracts } from './ipc-api'
// Implement invoke handlers (request/response)
class RegisterHandler extends AbstractRegisterHandler {
handlers: IPCHandlerType<InvokeContracts> = {
AddNumbers: async (_event, params) => {
return params.a + params.b
},
GetVersion: async () => {
return app.getVersion()
},
}
}
// Implement event handlers (renderer → main)
class RegisterEvent extends AbstractRegisterEvent {
events: IPCEventType<EventContracts> = {
Quit: () => {
app.quit()
},
LogMessage: (_event, message) => {
console.log(`Renderer: ${message}`)
},
}
}
// Implement stream invoke handlers (request with streaming response)
class RegisterStreamHandler extends AbstractRegisterStreamHandler {
handlers: IPCStreamHandlerType<StreamInvokeContracts> = {
GetLargeData: async (_event, { offset }) => {
// Return a ReadableStream
return createReadableStreamFromLargeData(offset)
},
}
}
// Implement stream upload handlers (renderer → main)
class RegisterStreamUpload extends AbstractRegisterStreamUpload {
handlers: IPCStreamUploadHandlerType<StreamUploadContracts> = {
UploadFile: ({ filename }, onData, onEnd, onError) => {
const chunks: Uint8Array[] = []
onData((chunk) => {
chunks.push(chunk)
})
onEnd(() => {
// Process uploaded data...
console.log(`Upload complete for ${filename} (${chunks.length} chunks)`)
})
onError((err) => {
console.error(`Upload failed for ${filename}`, err)
})
},
}
}
// Implement stream download handlers (main → renderer)
class RegisterStreamDownload extends AbstractRegisterStreamDownload {
handlers: IPCStreamDownloadHandlerType<StreamDownloadContracts> = {
DownloadLogs: async ({ sinceMs }, _event) => {
// Return a ReadableStream for logs
return createLogStream(sinceMs)
},
}
}
// Register all handlers
RegisterHandler.register()
RegisterEvent.register()
RegisterStreamHandler.register()
RegisterStreamUpload.register()
RegisterStreamDownload.register()Optional: typed handler helpers
You can keep handlers in plain objects and still get full typing:
import {
defineEventHandlers,
defineInvokeHandlers,
defineStreamDownloadHandlers,
} from '@number10/electron-ipc'
const invokeHandlers = defineInvokeHandlers<InvokeContracts>({
AddNumbers: async (_event, params) => params.a + params.b,
GetVersion: async () => app.getVersion(),
})
const eventHandlers = defineEventHandlers<EventContracts>({
Quit: () => app.quit(),
LogMessage: (_event, message) => console.log(message),
})
const downloadHandlers = defineStreamDownloadHandlers<StreamDownloadContracts>({
DownloadLogs: async ({ sinceMs }, _event) => createLogStream(sinceMs),
})
class RegisterHandler extends AbstractRegisterHandler {
handlers = invokeHandlers
}
class RegisterEvent extends AbstractRegisterEvent {
events = eventHandlers
}
class RegisterStreamDownload extends AbstractRegisterStreamDownload {
handlers = downloadHandlers
}Optional: runtime validation helpers
If you want runtime checks (Zod/Valibot/etc.), wrap handlers with validators. This keeps your contracts unchanged, but fails fast when payloads do not match. The helpers are validator-agnostic; any function that returns { success, data } works.
import { z } from 'zod'
import {
defineInvokeHandlers,
validatorFromSafeParse,
withInvokeValidation,
} from '@number10/electron-ipc'
const requestValidator = validatorFromSafeParse(
z.object({ a: z.number(), b: z.number() }).safeParse
)
const responseValidator = validatorFromSafeParse(z.number().safeParse)
const invokeHandlers = defineInvokeHandlers<InvokeContracts>({
AddNumbers: withInvokeValidation(
{ request: requestValidator, response: responseValidator },
async (_event, { a, b }) => a + b
),
})import { defineStreamUploadHandlers, withStreamUploadValidation } from '@number10/electron-ipc'
const uploadHandlers = defineStreamUploadHandlers<StreamUploadContracts>({
UploadFile: withStreamUploadValidation(
{ request: requestValidator, data: validatorFromSafeParse(z.instanceof(Uint8Array).safeParse) },
(_request, onData, onEnd, onError) => {
onError((err) => console.error(err))
onData((chunk) => {
// Handle validated chunks
})
onEnd(() => {
// Finalize upload
})
}
),
})Note: For stream invoke/download handlers, pass { data: validator } to validate each stream chunk.
// Option 1: Use generated main broadcast API (recommended)
import { mainBroadcast } from './broadcast-generated'
mainBroadcast.Ping(mainWindow, 42) // with payload
mainBroadcast.About(mainWindow) // void payload omitted
// Option 2: Use runtime-generated broadcast helper (alternative)
import { createBroadcast } from '@number10/electron-ipc'
const mainBroadcast = createBroadcast<BroadcastContracts>()
mainBroadcast('Ping', mainWindow, 42)
mainBroadcast('About', mainWindow, undefined)Note: The generated broadcast API (mainBroadcastOutput) is optional but recommended for consistency with the renderer API. Both approaches are type-safe.
CLI Usage
The generator uses YAML configuration files:
electron-ipc-generate --config=./ipc-config.yamlConfiguration Format
# ipc-config.yaml
apis:
- name: api # API name (used for expose function)
input: ./src/main/ipc-api.ts # Source file with contracts
output: ./src/preload/api-generated.ts # Generated preload API
tsconfig: ./tsconfig.json # Optional: path aliases/re-exports
contracts:
invoke: InvokeContracts # Optional: Invoke contract type
event: EventContracts # Optional: Event contract type
send: BroadcastContracts # Optional: Broadcast contract type
streamInvoke: StreamInvokeContracts # Optional: Stream invoke contracts
streamUpload: StreamUploadContracts # Optional: Stream upload contracts
streamDownload: StreamDownloadContracts # Optional: Stream download contracts
mainBroadcastOutput: ./src/main/broadcast-generated.ts # Optional: Main broadcast API
reactHooksOutput: ./src/preload/api-react-hooks.ts # Optional: Generated React hooksNote: At least one contract type must be specified per API. The mainBroadcastOutput is optional; if omitted, use the runtime createBroadcast() helper instead.
Integration References
Guides & Documentation
- Electron + Vite - Integration guide for electron-vite
- Electron Forge - Integration guide for Electron Forge
Working Example Apps
These apps are repository references for maintainers and advanced users. You do not need them to install or use @number10/electron-ipc in your own Electron app.
Full-Featured Apps:
apps/test-app- Main demo with React, all features, Inspector integrationapps/multi-window- Multi-window IPC flows, window registry, role-based broadcastsapps/high-volume-test- Inspector performance testing under high load
Minimal Bundler Examples:
apps/esbuild-minimal- Type-safe IPC with esbuild bundlerapps/webpack-minimal- Type-safe IPC with webpack bundlerapps/parcel-minimal- Type-safe IPC with Parcel 2.x bundler
Template:
packages/template-basic- Self-generating template showing best practices
Each example includes its own README with specific setup instructions.
Benefits
✅ Five Communication Patterns - Invoke (request-response), Events (fire-and-forget), Broadcasts (main → renderer), Renderer-to-Renderer, Streams (large data/real-time) ✅ Compile-Time Type Safety - Change a contract interface → TypeScript shows errors immediately in all usages
✅ No Runtime Surprises - Catch signature mismatches before running the app
✅ IntelliSense Everywhere - Auto-completion in main, preload, and renderer processes
✅ Refactoring Support - Rename/change contracts → TypeScript guides you to fix all usages
✅ Zero Boilerplate - Auto-generated IPC wrappers and type definitions
✅ Single Source of Truth - IPC contracts defined once, validated everywhere ✅ React Hooks - Automatic generation of useXxx hooks for renderer
Example: Type Safety in Action
Change this contract:
export type InvokeContracts = GenericInvokeContract<{
AddNumbers: IInvokeContract<{ a: number; b: number }, number>
}>To this:
export type InvokeContracts = GenericInvokeContract<{
AddNumbers: IInvokeContract<{ x: number; y: number }, number> // Changed a,b → x,y
}>TypeScript immediately shows compile errors in your handler implementation:
class RegisterHandler extends AbstractRegisterHandler {
handlers: IPCHandlerType<InvokeContracts> = {
AddNumbers: async (_event, v) => {
return v.a + v.b // ❌ Error: Property 'a' does not exist on type '{ x: number; y: number }'
},
}
}The same applies to:
- Renderer calls - Wrong parameters show errors in React components
- Broadcast usage - Type-checked at call site
- Event handlers - Payload types validated
No need to run the app to find these bugs! 🎯
Repository Development
This section is only needed when you want to work on the electron-ipc repository itself.
Prerequisites
- Node.js ≥20.19.0
- pnpm ≥8.0.0
- Windows only: Git Bash (for Git hooks)
Note for Windows users: This project uses Husky for Git hooks. Git Bash must be installed and available in your PATH for the pre-commit hooks to work properly.
Installation
# Install dependencies
pnpm install
# Build packages
pnpm run build
# Run tests
pnpm run test
# Start test app
pnpm run devWorking on Individual Packages
# electron-ipc library
cd packages/electron-ipc
pnpm run build
pnpm run watch
# test-app
cd apps/test-app
pnpm run devProject Structure
electron-ipc/
├── packages/
│ ├── electron-ipc/ # Main library (publishable to npm)
│ │ ├── src/
│ │ │ ├── generator/ # Code generation logic
│ │ │ ├── interfaces/ # TypeScript interfaces
│ │ │ ├── window-manager/ # Window registry module
│ │ │ ├── inspector/ # IPC Inspector module
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ └── template-basic/ # Self-generating template
│
├── apps/ # Example applications
│ ├── test-app/ # Main test app (electron-vite)
│ │ ├── src/
│ │ │ ├── main/ # Main process code
│ │ │ ├── preload/ # Preload scripts
│ │ │ └── renderer/ # Renderer process (React)
│ │ └── public/ # React components
│ │
│ ├── multi-window/ # Multi-window demo (electron-vite)
│ ├── high-volume-test/ # Inspector stress test (electron-vite)
│ ├── esbuild-minimal/ # Minimal example with esbuild
│ ├── webpack-minimal/ # Minimal example with webpack
│ └── parcel-minimal/ # Minimal example with Parcel 2.x
│
├── docs-site/ # Documentation site (VitePress)
├── package.json # Workspace root
└── tsconfig.json # Base TypeScript config🛠 Technology Stack
- TypeScript - Strict mode, ES2022
- Vite - Build tool for library
- electron-vite - Build tool for Electron app
- React - UI framework for test app
- Vitest - Testing framework
- ESLint - Code linting (flat config)
- Prettier - Code formatting (no semicolons)
- Husky - Git hooks
- ts-morph - TypeScript AST manipulation
- yaml - YAML configuration parsing
🤝 Contributing
- Create feature branch
- Make changes
- Run
pnpm run lintandpnpm run test - Commit with conventional commit format:
feat:new featurefix:bug fixdocs:documentationrefactor:code refactoringtest:testingchore:maintenance
📝 License
MIT
Common Mistakes (and fixes)
- Using non-serializable types: avoid
Date,Map,Set, and class instances across IPC; send ISO strings or plain objects instead. - Missing
contextIsolation: IPC APIs should be exposed via preload andcontextBridge, not directly in the renderer. - Forgetting
tsconfigin YAML: addtsconfigwhen you rely on path aliases or re-exports. - Misreading prefixes: generated methods add
invoke/send/on/rendererInvoke/invokeStream/upload/downloadprefixes. - Not registering handlers: make sure
RegisterHandler.register()(and other register classes) are called in main.