Skip to content
Open
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
36 changes: 36 additions & 0 deletions packages/plugin/vite/spec/config/vite.base.config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,5 +235,41 @@ describe('vite.base.config', () => {

expect(consoleWarn).not.toHaveBeenCalled();
});

describe('in the watch subprocess of electron-forge start', () => {
afterEach(() => {
vi.unstubAllEnvs();
process.send = undefined;
});

it('asks the parent to restart the app after a rebuild', () => {
const handler = handleRestarts();
vi.stubEnv('FORGE_VITE_WATCH', '1');
const send = vi.fn(() => true);
process.send = send;
const plugin = pluginHotRestart('restart');

// The first build runs before the parent spawns the app.
closeBundle(plugin)();
expect(send).not.toHaveBeenCalled();

closeBundle(plugin)();
expect(send).toHaveBeenCalledExactlyOnceWith({ type: 'restart' });
// The restart handler is registered in the parent, not here.
expect(handler).not.toHaveBeenCalled();
});

it('does not ask the parent to restart the app when the rebuild failed', () => {
vi.stubEnv('FORGE_VITE_WATCH', '1');
const send = vi.fn(() => true);
process.send = send;
const plugin = pluginHotRestart('restart');

closeBundle(plugin)();
closeBundle(plugin)(new Error('syntax error'));

expect(send).not.toHaveBeenCalled();
});
});
});
});
55 changes: 55 additions & 0 deletions packages/plugin/vite/spec/subprocess-worker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { afterEach, describe, expect, it } from 'vitest';
Expand Down Expand Up @@ -184,6 +185,60 @@ describe('subprocess-worker', () => {
expect(contents).toContain('from-secondary');
});

it('asks the parent to restart the app after a main rebuild with hotRestart', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-vite-watch-'));
fs.cpSync(projectDir, tmpDir, { recursive: true });
const config: Pick<VitePluginConfig, 'build' | 'renderer' | 'hotRestart'> =
{
build: [
{
entry: 'src/main.js',
config: path.join(tmpDir, 'vite.main.config.mjs'),
target: 'main',
},
],
renderer: [],
hotRestart: true,
};

const child = spawn(process.execPath, [workerPath], {
cwd: tmpDir,
env: {
...process.env,
FORGE_VITE_PROJECT_DIR: tmpDir,
FORGE_VITE_KIND: 'build',
FORGE_VITE_INDEX: '0',
FORGE_VITE_CONFIG: JSON.stringify(config),
FORGE_VITE_WATCH: '1',
},
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
});
const messages: string[] = [];
const waitFor = (type: string) =>
new Promise<void>((resolve) => {
child.on('message', (msg: { type: string }) => {
if (msg.type === type) resolve();
});
});
child.on('message', (msg: { type: string }) => messages.push(msg.type));

try {
await waitFor('first-build-done');
await new Promise((resolve) => setTimeout(resolve, 500));
expect(messages).toEqual(['first-build-done']);

const restarted = waitFor('restart');
fs.appendFileSync(path.join(tmpDir, 'src', 'main.js'), '\n// rebuild\n');
await restarted;
expect(messages).toEqual(['first-build-done', 'restart']);
} finally {
const closed = new Promise((resolve) => child.once('close', resolve));
child.kill();
await closed;
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it('exits nonzero and surfaces error when build fails', async () => {
const config: Pick<VitePluginConfig, 'build' | 'renderer'> = {
build: [
Expand Down
20 changes: 14 additions & 6 deletions packages/plugin/vite/src/VitePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import path from 'node:path';
import { styleText } from 'node:util';

import { readJson, writeJson } from '@electron-forge/core-utils';
import { requestAppRestart } from '@electron-forge/core-utils/restart';
import { namedHookWithTaskFn, PluginBase } from '@electron-forge/plugin-base';
import debug from 'debug';
import { Listr, PRESET_TIMER } from 'listr2';
import * as vite from 'vite';

import { viteDevServerUrls } from './config/vite.base.config.js';
import {
viteDevServerUrls,
warnAppNotRestarted,
} from './config/vite.base.config.js';
import ViteConfigGenerator from './ViteConfig.js';

import type { VitePluginBuildConfig, VitePluginConfig } from './Config.js';
Expand Down Expand Up @@ -108,7 +112,7 @@ function spawnViteBuild(
}

function spawnViteBuildWatch(
pluginConfig: Pick<VitePluginConfig, 'build' | 'renderer'>,
pluginConfig: Pick<VitePluginConfig, 'build' | 'renderer' | 'hotRestart'>,
index: number,
projectDir: string,
devServerUrls: Record<string, string>,
Expand Down Expand Up @@ -153,6 +157,8 @@ function spawnViteBuildWatch(
settle(() => reject(new Error(msg.message)));
} else if (msg.type === 'reload-renderers') {
onReloadRenderers();
} else if (msg.type === 'restart' && !requestAppRestart()) {
warnAppNotRestarted();
}
});
child.on('error', (err) => settle(() => reject(err)));
Expand Down Expand Up @@ -373,17 +379,19 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}.`);

/**
* Serializable snapshot of the plugin config to pass to subprocess workers.
* We only include build[] and renderer[] — the worker needs the full renderer
* list for defines even when building a single main target. `hotRestart` is
* moot here: workers only run when packaging.
* We include build[] and renderer[] — the worker needs the full renderer
* list for defines even when building a single main target — and
* `hotRestart`, which the watch builds of `electron-forge start` need to add
* the hot restart plugin to main targets.
*/
private get serializableConfig(): Pick<
VitePluginConfig,
'build' | 'renderer'
'build' | 'renderer' | 'hotRestart'
> {
return {
build: this.config.build,
renderer: this.config.renderer,
hotRestart: this.config.hotRestart,
};
}

Expand Down
26 changes: 20 additions & 6 deletions packages/plugin/vite/src/config/vite.base.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
};
return { ...acc, ...def };
},
{} as Record<string, any>,

Check warning on line 72 in packages/plugin/vite/src/config/vite.base.config.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

typescript(no-explicit-any)

packages/plugin/vite/src/config/vite.base.config.ts:72:26: Unexpected `any`. Specify a different type.
);

return define;
Expand Down Expand Up @@ -108,6 +108,18 @@
};
}

/**
* Warn that a rebuilt main process bundle never reached the running app.
*/
export function warnAppNotRestarted(): void {
console.warn(
styleText(
'yellow',
'[@electron-forge/plugin-vite] Rebuilt the main process bundle, but the running app was not restarted, so it is still running the previous code.',
),
);
}

export function pluginHotRestart(command: 'reload' | 'restart'): Plugin {
let builtOnce = false;

Expand All @@ -127,15 +139,17 @@
// Preload scripts hot reload.
server.ws.send({ type: 'full-reload' });
}
} else if (
command === 'restart' &&
process.env.FORGE_VITE_WATCH === '1'
) {
// `electron-forge start` builds main targets in a watch subprocess, but
// the restart handler is registered in the parent; ask it to restart.
if (isRebuild) process.send?.({ type: 'restart' });
} else if (command === 'restart' && !requestAppRestart() && isRebuild) {
// The first build finishes before the app is spawned, so only a rebuild
// that fails to reach it is worth warning about.
console.warn(
styleText(
'yellow',
'[@electron-forge/plugin-vite] Rebuilt the main process bundle, but the running app was not restarted, so it is still running the previous code.',
),
);
warnAppNotRestarted();
}
},
};
Expand Down
Loading