Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/plugin/vite/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion packages/plugin/vite/src/VitePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -108,7 +109,7 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {
await Promise.all(
(
await this.configGenerator.getBuildConfig(watch)
).map((userConfig) => {
).map((userConfig, index) => {
return new Promise<void>((resolve, reject) => {
vite
.build({
Expand All @@ -124,6 +125,7 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {
// TODO: implement hot-restart here
},
},
...(this.isProd ? [] : [hotRestart(this.config.build[index])]),
...(userConfig.plugins ?? []),
],
})
Expand Down
32 changes: 32 additions & 0 deletions packages/plugin/vite/src/util/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { builtinModules } from 'node:module';

import type { VitePluginBuildConfig } from '../Config';
import type { Plugin } from 'vite';

/**
Expand Down Expand Up @@ -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 <Plugin>{
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();
}
},
};
}
3 changes: 3 additions & 0 deletions packages/plugin/vite/test/fixture/lib-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function booststrap() {
console.log('App bootstrap.');
}
89 changes: 82 additions & 7 deletions packages/plugin/vite/test/util/plugins_spec.ts
Original file line number Diff line number Diff line change
@@ -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) =>
Expand Down Expand Up @@ -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<RollupWatcher>(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');
});
});