alex8088 / electron-vite

Next generation Electron build tooling based on Vite 新一代 Electron 开发构建工具,支持源代码保护
https://electron-vite.org
MIT License
3.39k stars 146 forks source link

Auto generate exposeInMainWorld typings for preload scripts exports #141

Open syabro opened 1 year ago

syabro commented 1 year ago

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 }


3.  Update `index.d.ts` to use global `Api`
```ts
// 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
  2. 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 commented 1 year ago

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 commented 1 year ago

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')
            }
          )
        }
      }
    ],
    // ...
  },
  // ...
})
subframe7536 commented 1 year ago

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

alex8088 commented 1 year ago
// preload/index.d.ts
import { ElectronAPI } from '@electron-toolkit/preload'

declare global {
  interface Window {
    electron: ElectronAPI
    api: typeof preloadApi
  }
}
alex8088 commented 1 year ago

duplicate #121

syabro commented 1 year ago

@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
douglascayers commented 1 year ago

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

starknt commented 1 year ago

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