electron-vite icon indicating copy to clipboard operation
electron-vite copied to clipboard

Auto generate exposeInMainWorld typings for preload scripts exports

Open syabro opened this issue 2 years ago • 8 comments

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

  1. 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
}
  1. 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
}
  1. Update index.d.ts to use global Api
// index.d.ts
import { ElectronAPI } from '@electron-toolkit/preload'

declare global {
  interface Window {
    electron: ElectronAPI
    api: Api
  }
}
  1. Generate api types
tsc --declaration --emitDeclarationOnly --outDir ./src/preload/ ./src/preload/api.ts -w
  1. Have fun image

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

syabro avatar Apr 12 '23 05:04 syabro

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...

syabro avatar Apr 12 '23 05:04 syabro

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')
            }
          )
        }
      }
    ],
    // ...
  },
  // ...
})

syabro avatar Apr 12 '23 05:04 syabro

maybe help https://github.com/cawa-93/unplugin-auto-expose

subframe7536 avatar Apr 17 '23 12:04 subframe7536

// preload/index.d.ts
import { ElectronAPI } from '@electron-toolkit/preload'

declare global {
  interface Window {
    electron: ElectronAPI
    api: typeof preloadApi
  }
}

alex8088 avatar Apr 18 '23 04:04 alex8088

duplicate #121

alex8088 avatar Apr 18 '23 04:04 alex8088

@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 avatar Apr 18 '23 04:04 syabro

@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.

douglascayers avatar Sep 24 '23 23:09 douglascayers

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:

  1. Automation, find entry scripts. And implement this goal we may need some conventions(like nuxt 3).
  2. Generate d.ts file

starknt avatar Oct 13 '23 12:10 starknt