Skip to content

Commit

Permalink
feat(plugin-vite): hot-restart 🔥
Browse files Browse the repository at this point in the history
  • Loading branch information
caoxiemeihao committed Aug 19, 2023
1 parent 0cd8fb7 commit bd0d802
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 48 deletions.
24 changes: 24 additions & 0 deletions packages/plugin/vite/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ 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 `args.restart` provided by the function.
*/
restart?:
| false
| ((args: {
/**
* Restart the entire Electron App.
*/
restart: () => void;
/**
* When a Preload script is rebuilt, users can refresh the Renderer process by `ViteDevServer` instead of restart the entire Electron App.
*
* @example
* ```ts
* renderer.find(({ config }) => config.name === 'main_window').reload();
* ```
*/
renderer: {
config: VitePluginRendererConfig;
reload: () => void;
}[];
}) => void);
}

export interface VitePluginRendererConfig {
Expand Down
47 changes: 34 additions & 13 deletions packages/plugin/vite/src/ViteConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'node:path';
import debug from 'debug';
import { ConfigEnv, loadConfigFromFile, mergeConfig, UserConfig } from 'vite';

import { VitePluginConfig } from './Config';
import { VitePluginBuildConfig, VitePluginConfig, VitePluginRendererConfig } from './Config';
import { externalBuiltins } from './util/plugins';

const d = debug('electron-forge:plugin:vite:viteconfig');
Expand All @@ -14,10 +14,20 @@ const d = debug('electron-forge:plugin:vite:viteconfig');
*/
export type LoadResult = Awaited<ReturnType<typeof loadConfigFromFile>>;

export interface BuildConfig {
config: VitePluginBuildConfig;
vite: UserConfig;
}

export interface RendererConfig {
config: VitePluginRendererConfig;
vite: UserConfig;
}

export default class ViteConfigGenerator {
private readonly baseDir: string;

private rendererConfigCache!: Promise<UserConfig>[];
private rendererConfigCache!: Promise<RendererConfig>[];

constructor(private readonly pluginConfig: VitePluginConfig, private readonly projectDir: string, private readonly isProd: boolean) {
this.baseDir = path.join(projectDir, '.vite');
Expand All @@ -43,20 +53,21 @@ export default class ViteConfigGenerator {
async getDefines(): Promise<Record<string, string>> {
const defines: Record<string, any> = {};
const rendererConfigs = await this.getRendererConfig();
for (const [index, userConfig] of rendererConfigs.entries()) {
for (const [index, { vite: viteConfig }] of rendererConfigs.entries()) {
const name = this.pluginConfig.renderer[index].name;
if (!name) {
continue;
}
const NAME = name.toUpperCase().replace(/ /g, '_');
// 🎯-①:
// `server.port` is set in `launchRendererDevServers` in `VitePlugin.ts`.
defines[`${NAME}_VITE_DEV_SERVER_URL`] = this.isProd ? undefined : JSON.stringify(`http://localhost:${userConfig?.server?.port}`);
defines[`${NAME}_VITE_DEV_SERVER_URL`] = this.isProd ? undefined : JSON.stringify(`http://localhost:${viteConfig?.server?.port}`);
defines[`${NAME}_VITE_NAME`] = JSON.stringify(name);
}
return defines;
}

async getBuildConfig(watch = false): Promise<UserConfig[]> {
async getBuildConfig(watch = false): Promise<BuildConfig[]> {
if (!Array.isArray(this.pluginConfig.build)) {
throw new Error('"config.build" must be an Array');
}
Expand All @@ -65,8 +76,9 @@ export default class ViteConfigGenerator {
const plugins = [externalBuiltins()];
const configs = this.pluginConfig.build
.filter(({ entry, config }) => entry || config)
.map<Promise<UserConfig>>(async ({ entry, config }) => {
const defaultConfig: UserConfig = {
.map<Promise<BuildConfig>>(async (buildConfig) => {
const { entry, config } = buildConfig;
let viteConfig: UserConfig = {
// Ensure that each build config loads the .env file correctly.
mode: this.mode,
build: {
Expand All @@ -90,21 +102,25 @@ export default class ViteConfigGenerator {
};
if (config) {
const loadResult = await this.resolveConfig(config);
return mergeConfig(defaultConfig, loadResult?.config ?? {});
viteConfig = mergeConfig(viteConfig, loadResult?.config ?? {});
}
return defaultConfig;
return {
config: buildConfig,
vite: viteConfig,
};
});

return await Promise.all(configs);
}

async getRendererConfig(): Promise<UserConfig[]> {
async getRendererConfig(): Promise<RendererConfig[]> {
if (!Array.isArray(this.pluginConfig.renderer)) {
throw new Error('"config.renderer" must be an Array');
}

const configs = (this.rendererConfigCache ??= this.pluginConfig.renderer.map(async ({ name, config }) => {
const defaultConfig: UserConfig = {
const configs = (this.rendererConfigCache ??= this.pluginConfig.renderer.map(async (rendererConfig) => {
const { name, config } = rendererConfig;
let viteConfig: UserConfig = {
// Ensure that each build config loads the .env file correctly.
mode: this.mode,
// Make sure that Electron can be loaded into the local file using `loadFile` after packaging.
Expand All @@ -115,7 +131,12 @@ export default class ViteConfigGenerator {
clearScreen: false,
};
const loadResult = (await this.resolveConfig(config)) ?? { path: '', config: {}, dependencies: [] };
return mergeConfig(defaultConfig, loadResult.config);
viteConfig = mergeConfig(viteConfig, loadResult.config);

return {
config: rendererConfig,
vite: viteConfig,
};
}));

return await Promise.all(configs);
Expand Down
48 changes: 31 additions & 17 deletions packages/plugin/vite/src/VitePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,20 @@ import debug from 'debug';
import { RollupWatcher } from 'rollup';
import { default as vite } from 'vite';

import { VitePluginConfig } from './Config';
import { VitePluginConfig, VitePluginRendererConfig } from './Config';
import { hotRestart } from './util/plugins';
import ViteConfigGenerator from './ViteConfig';

const d = debug('electron-forge:plugin:vite');

// 🎯-②:
// Make sure the `server` is bound to the `renderer` in forge.config.ts,
// It's used to refresh the Renderer process, which one can be determined according to `config`.
export interface RendererConfigWithServer {
config: VitePluginRendererConfig;
server: vite.ViteDevServer;
}

export default class VitePlugin extends PluginBase<VitePluginConfig> {
private static alreadyStarted = false;

Expand All @@ -31,7 +40,7 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {

private watchers: RollupWatcher[] = [];

private servers: vite.ViteDevServer[] = [];
public renderers: RendererConfigWithServer[] = [];

init = (dir: string): void => {
this.setDirectories(dir);
Expand Down Expand Up @@ -105,26 +114,27 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {

// Main process, Preload scripts and Worker process, etc.
build = async (watch = false): Promise<void> => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
await Promise.all(
(
await this.configGenerator.getBuildConfig(watch)
).map((userConfig) => {
).map(({ config, vite: viteConfig }) => {
return new Promise<void>((resolve, reject) => {
vite
.build({
// Avoid recursive builds caused by users configuring @electron-forge/plugin-vite in Vite config file.
configFile: false,
...userConfig,
...viteConfig,
plugins: [
{
name: '@electron-forge/plugin-vite:start',
closeBundle() {
resolve();

// TODO: implement hot-restart here
},
},
...(userConfig.plugins ?? []),
...(this.isProd ? [hotRestart(config, that)] : []),
...(viteConfig.plugins ?? []),
],
})
.then((result) => {
Expand All @@ -144,34 +154,38 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {

// Renderer process
buildRenderer = async (): Promise<void> => {
for (const userConfig of await this.configGenerator.getRendererConfig()) {
for (const { vite: viteConfig } of await this.configGenerator.getRendererConfig()) {
await vite.build({
configFile: false,
...userConfig,
...viteConfig,
});
}
};

launchRendererDevServers = async (): Promise<void> => {
for (const userConfig of await this.configGenerator.getRendererConfig()) {
for (const { config, vite: viteConfig } of await this.configGenerator.getRendererConfig()) {
const viteDevServer = await vite.createServer({
configFile: false,
...userConfig,
...viteConfig,
});

await viteDevServer.listen();
viteDevServer.printUrls();

this.servers.push(viteDevServer);
this.renderers.push({
config,
server: viteDevServer,
});

if (viteDevServer.httpServer) {
// 🎯-①:
// Make suee that `getDefines` in VitePlugin.ts gets the correct `server.port`. (#3198)
const addressInfo = viteDevServer.httpServer.address();
const isAddressInfo = (x: any): x is AddressInfo => x?.address;

if (isAddressInfo(addressInfo)) {
userConfig.server ??= {};
userConfig.server.port = addressInfo.port;
viteConfig.server ??= {};
viteConfig.server.port = addressInfo.port;
}
}
}
Expand All @@ -186,11 +200,11 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {
}
this.watchers = [];

for (const server of this.servers) {
for (const renderer of this.renderers) {
d('cleaning http server');
server.close();
renderer.server.close();
}
this.servers = [];
this.renderers = [];
}
if (err) console.error(err.stack);
// Why: This is literally what the option says to do.
Expand Down
47 changes: 47 additions & 0 deletions packages/plugin/vite/src/util/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { builtinModules } from 'node:module';

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

/**
Expand Down Expand Up @@ -33,3 +35,48 @@ export function externalBuiltins() {
},
};
}

/**
* Hot restart App during development for better DX.
*/
export function hotRestart(
config: VitePluginBuildConfig,
// For `VitePluginBuildConfig['restart']` callback args.
context: { renderers: RendererConfigWithServer[] }
) {
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,
renderer: context.renderers.map((renderer) => ({
// 🎯-②:
// Users can decide which Renderer process to refresh according to `config`.
config: renderer.config,
reload() {
renderer.server.ws.send({ type: 'full-reload' });
},
})),
});
} else {
restart();
}
},
};
}
25 changes: 14 additions & 11 deletions packages/plugin/vite/test/ViteConfig_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ describe('ViteConfigGenerator', () => {
const generator = new ViteConfigGenerator(config, '', false);
const servers: ViteDevServer[] = [];

for (const userConfig of await generator.getRendererConfig()) {
for (const { vite: viteConfig } of await generator.getRendererConfig()) {
const viteDevServer = await vite.createServer({
configFile: false,
...userConfig,
optimizeDeps: {
disabled: true,
},
...viteConfig,
});

await viteDevServer.listen();
viteDevServer.printUrls();
// viteDevServer.printUrls();
servers.push(viteDevServer);

// Make suee that `getDefines` in VitePlugin.ts gets the correct `server.port`. (#3198)
const addressInfo = viteDevServer.httpServer!.address() as AddressInfo;
userConfig.server ??= {};
userConfig.server.port = addressInfo.port;
viteConfig.server ??= {};
viteConfig.server.port = addressInfo.port;
}

const define = await generator.getDefines();
Expand All @@ -50,15 +53,15 @@ describe('ViteConfigGenerator', () => {
renderer: [],
} as VitePluginConfig;
const generator = new ViteConfigGenerator(config, '', true);
const buildConfig = (await generator.getBuildConfig())[0];
expect(buildConfig).deep.equal({
const { vite: viteConfig } = (await generator.getBuildConfig())[0];
expect(viteConfig).deep.equal({
mode: 'production',
build: {
lib: {
entry: 'foo.js',
formats: ['cjs'],
// shims
fileName: (buildConfig.build?.lib as any)?.fileName,
fileName: (viteConfig.build?.lib as any)?.fileName,
},
emptyOutDir: false,
outDir: path.join('.vite', 'build'),
Expand All @@ -67,7 +70,7 @@ describe('ViteConfigGenerator', () => {
clearScreen: false,
define: {},
// shims
plugins: [buildConfig.plugins?.[0]],
plugins: viteConfig.plugins,
} as UserConfig);
});

Expand All @@ -77,8 +80,8 @@ describe('ViteConfigGenerator', () => {
} as VitePluginConfig;
const generator = new ViteConfigGenerator(config, '', false);
const configs = await generator.getRendererConfig();
for (const [index, rendererConfig] of configs.entries()) {
expect(rendererConfig).deep.equal({
for (const [index, { vite: viteConfig }] of configs.entries()) {
expect(viteConfig).deep.equal({
mode: 'development',
base: './',
build: {
Expand Down
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.');
}
Loading

0 comments on commit bd0d802

Please sign in to comment.