diff --git a/packages/plugin/vite/src/Config.ts b/packages/plugin/vite/src/Config.ts index 2f1ffd3601..9e4cf02820 100644 --- a/packages/plugin/vite/src/Config.ts +++ b/packages/plugin/vite/src/Config.ts @@ -9,6 +9,11 @@ export interface VitePluginBuildConfig { * Vite config file path. */ config?: string; + /** + * By default, when any entry in `build` is rebuilt it will restart the Electron App. + * If you want to customize this behavior, you can pass a function and control it with the `rs` provided by the callback. + */ + restart?: false | ((rs: () => void) => void); } export interface VitePluginRendererConfig { diff --git a/packages/plugin/vite/src/VitePlugin.ts b/packages/plugin/vite/src/VitePlugin.ts index 0625915855..a2ebaf6023 100644 --- a/packages/plugin/vite/src/VitePlugin.ts +++ b/packages/plugin/vite/src/VitePlugin.ts @@ -10,6 +10,7 @@ import { RollupWatcher } from 'rollup'; import { default as vite } from 'vite'; import { VitePluginConfig } from './Config'; +import { hotRestart } from './util/plugins'; import ViteConfigGenerator from './ViteConfig'; const d = debug('electron-forge:plugin:vite'); @@ -108,7 +109,7 @@ export default class VitePlugin extends PluginBase { await Promise.all( ( await this.configGenerator.getBuildConfig(watch) - ).map((userConfig) => { + ).map((userConfig, index) => { return new Promise((resolve, reject) => { vite .build({ @@ -124,6 +125,7 @@ export default class VitePlugin extends PluginBase { // TODO: implement hot-restart here }, }, + ...(this.isProd ? [] : [hotRestart(this.config.build[index])]), ...(userConfig.plugins ?? []), ], }) diff --git a/packages/plugin/vite/src/util/plugins.ts b/packages/plugin/vite/src/util/plugins.ts index fb5df3db1b..9959427fe0 100644 --- a/packages/plugin/vite/src/util/plugins.ts +++ b/packages/plugin/vite/src/util/plugins.ts @@ -1,5 +1,6 @@ import { builtinModules } from 'node:module'; +import type { VitePluginBuildConfig } from '../Config'; import type { Plugin } from 'vite'; /** @@ -33,3 +34,34 @@ export function externalBuiltins() { }, }; } + +/** + * Hot restart App during development for better DX. + */ +export function hotRestart(config: VitePluginBuildConfig) { + const restart = () => { + // https://github.com/electron/forge/blob/v6.1.1/packages/api/core/src/api/start.ts#L204-L211 + process.stdin.emit('data', 'rs'); + }; + // Avoid first start, it's stated by forge. + let isFirstStart: undefined | true; + + return { + name: '@electron-forge/plugin-vite:hot-restart', + closeBundle() { + if (isFirstStart == null) { + isFirstStart = true; + return; + } + if (config.restart === false) { + return; + } + if (typeof config.restart === 'function') { + // Leave it to the user to decide whether to restart. + config.restart(restart); + } else { + restart(); + } + }, + }; +} diff --git a/packages/plugin/vite/test/fixture/lib-entry.ts b/packages/plugin/vite/test/fixture/lib-entry.ts new file mode 100644 index 0000000000..47ccd3c013 --- /dev/null +++ b/packages/plugin/vite/test/fixture/lib-entry.ts @@ -0,0 +1,3 @@ +export default function booststrap() { + console.log('App bootstrap.'); +} diff --git a/packages/plugin/vite/test/util/plugins_spec.ts b/packages/plugin/vite/test/util/plugins_spec.ts index bdf0eb226e..3267a64477 100644 --- a/packages/plugin/vite/test/util/plugins_spec.ts +++ b/packages/plugin/vite/test/util/plugins_spec.ts @@ -1,15 +1,19 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { builtinModules } from 'module'; +import fs from 'node:fs'; +import { builtinModules } from 'node:module'; +import path from 'node:path'; import { expect } from 'chai'; -// eslint-disable-next-line node/no-extraneous-import -import { ExternalOption } from 'rollup'; -import { resolveConfig } from 'vite'; +import { build, type Plugin, resolveConfig } from 'vite'; -import { externalBuiltins } from '../../src/util/plugins'; +import { externalBuiltins, hotRestart } from '../../src/util/plugins'; -describe('plugins', () => { - it('externalBuiltins', async () => { +import type { ExternalOption, RollupWatcher } from 'rollup'; + +export type RestartType = 'auto' | 'manually' | null; + +describe('interval Vite plugins', () => { + it('vite-plugin externalBuiltins', async () => { const nativeModules = builtinModules.filter((e) => !e.startsWith('_')); const builtins: any[] = ['electron', ...nativeModules, ...nativeModules.map((m) => `node:${m}`)]; const getConfig = (external: ExternalOption) => @@ -42,4 +46,75 @@ describe('plugins', () => { const external_function2 = (await getConfig(external_function))!.build!.rollupOptions!.external; expect((external_function2 as (source: string) => boolean)('electron')).true; }); + + it('vite-plugin hotRestart', async () => { + const createBuild = (plugin: Plugin) => + // eslint-disable-next-line no-async-promise-executor + new Promise(async (resolve) => { + let isFirstStart: undefined | true; + const root = path.join(__dirname, '../fixture'); + const entryFile = path.join(root, 'lib-entry.ts'); + const watcher = (await build({ + configFile: false, + root, + build: { + lib: { + entry: 'lib-entry.ts', + formats: ['cjs'], + }, + watch: {}, + }, + plugins: [ + // `hotStart` plugin + plugin, + { + name: 'close-watcher', + async closeBundle() { + if (isFirstStart == null) { + isFirstStart = true; + + // Trigger hot restart + setTimeout(() => { + fs.writeFileSync(entryFile, fs.readFileSync(entryFile, 'utf8')); + }, 100); + } else { + watcher.close(); + resolve(watcher); + } + }, + }, + ], + logLevel: 'silent', + })) as RollupWatcher; + }); + + const autoRestart = await (async () => { + let restart: RestartType | undefined; + // If directly manipulating `process.stdin` of the Main process will cause some side-effects, + // then this should be rewritten with a Child porcess. 🚧 + process.stdin.once('data', (data: Buffer) => { + if (data.toString().trim() === 'rs') { + restart = 'auto'; + } + process.stdin.destroy(); + }); + await createBuild(hotRestart({})); + return restart; + })(); + + const manuallyRestart = await (async () => { + let restart: RestartType | undefined; + await createBuild( + hotRestart({ + restart() { + restart = 'manually'; + }, + }) + ); + return restart; + })(); + + expect(autoRestart).equal('auto'); + expect(manuallyRestart).equal('manually'); + }); });