electron-vite
electron-vite copied to clipboard
Auto generate exposeInMainWorld typings for preload scripts exports
Clear and concise description of the problem
Currently api type is unknown
declare global {
interface Window {
electron: ElectronAPI
api: unknown
}
}
And I have to replace it with real api and keep them synced
Suggested solution
- extract api to a separate file
api.ts
// api.ts
import { ipcRenderer } from "electron";
export const preloadApi = {
ping: () => ipcRenderer.invoke('ping'),
}
declare global {
type Api = typeof preloadApi
}
- update preload src to use imported api instead of writing it in the same file
// index.ts
import { contextBridge } from "electron";
import { electronAPI } from "@electron-toolkit/preload";
import { api } from "./api"; // 👈🏻 new import
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}
- Update
index.d.tsto use globalApi
// index.d.ts
import { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron: ElectronAPI
api: Api
}
}
- Generate api types
tsc --declaration --emitDeclarationOnly --outDir ./src/preload/ ./src/preload/api.ts -w
- Have fun

Alternative
No response
Additional context
The problem here I don't know how to call api type compilation somewhere from vite without running tsc separately :(
Validations
- [X] Follow the Code of Conduct.
- [X] Read the Contributing Guidelines.
- [X] Read the docs.
- [X] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
Upd:
found https://vitejs.dev/guide/api-plugin.html#universal-hooks + closeBuild in https://github.com/vitejs/vite/discussions/9217
// vite.config.ts
export default defineConfig({
build: {
watch: {
include: 'src/**'
},
} ...
plugins: [
{
name: 'postbuild-commands', // the name of your custom plugin. Could be anything.
closeBundle: async () => {
await postBuildCommands() // run during closeBundle hook. https://rollupjs.org/guide/en/#closebundle
}
},
]
})
I guess we can write a tiny plugin that would open a process with tsc on on build start and close on build end...
UPD2: Used writeBundle hook, works :)
// electron.vite.config.ts
import { exec } from 'child_process'
// ...
export default defineConfig({
// ...
preload: {
plugins: [
// ...
{
name: 'rebuild-api-types',
writeBundle: (options: any, bundle: { [fileName: string]: any }) => {
exec(
'tsc --declaration --emitDeclarationOnly --outDir ./src/preload/ ./src/preload/api.ts',
(error, stdout, stderr) => {
if (error) return console.error(`Error: ${error.message}`)
if (stderr) return console.error(`Stderr: ${stderr}`)
console.log(' regenerated api.d.ts')
}
)
}
}
],
// ...
},
// ...
})
maybe help https://github.com/cawa-93/unplugin-auto-expose
// preload/index.d.ts
import { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron: ElectronAPI
api: typeof preloadApi
}
}
duplicate #121
@alex8088 I've upgraded my approach - it generates api from handlers automatically so less boilerplate
// src/preload/api.ts
import { ipcRenderer } from 'electron'
import { ipcHandlers } from '../src-main/handlers'
type ApiFromHandlers<T extends Record<string, (event: unknown, ...args: any[]) => any>> = {
[K in keyof T]: (
...args: Parameters<T[K]> extends [unknown, ...infer R] ? R : never
) => Promise<ReturnType<T[K]>>
}
function createApi<T extends Record<string, (event: unknown, ...args: any[]) => any>>(
handlers: T
): ApiFromHandlers<T> {
const api: Partial<ApiFromHandlers<T>> = {}
for (const key in handlers) {
api[key as keyof T] = (...args: any[]) => ipcRenderer.invoke(key as string, ...args)
}
return api as ApiFromHandlers<T>
}
export const api = createApi(ipcHandlers)
declare global {
type Api = typeof api
}
// src/main/ipcHandlers
export const ipcHandlers = {
downloadPreset: async (event: any, params: { packName: string, url: string, name: string, vst: string }) => {
},
checkPresetInstalled: (event, params: { packName: string, name: string, vst: string }): boolean | Error => {
},
}
export type IpcHandlers = typeof ipcHandlers
@syabro Thank you! I now have types for my preload api ⚡
@alex8088 Thank you for this great electron framework 😄
For automation, I added a typecheck:preload script to my package.json and called it from the existing typecheck script:
{
"scripts": {
"typecheck:preload": "tsc --declaration --emitDeclarationOnly --outDir ./src/preload/ ./src/preload/api.ts",
"typecheck": "yarn typecheck:preload && yarn typecheck:node && yarn typecheck:web",
...
}
}
Now if I make changes, I can just build the app and the typings will be regenerated for me.
I think we may have a better solution to this problem. For now, we are considering unplugin-auto-expose or unplugin-elexpse as a possible solution. But they have some limitations and don't work out of the box.
Considerations for out-of-the-box:
- Automation, find entry scripts. And implement this goal we may need some conventions(like nuxt 3).
- Generate
d.tsfile