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
55 changes: 55 additions & 0 deletions packages/plugin/vite/spec/ViteConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,61 @@ describe('ViteConfigGenerator', () => {
).toEqual(['@electron-forge/plugin-vite:hot-restart']);
});

describe('devtron', () => {
const devtronForgeConfig = (devtron?: boolean): VitePluginConfig => ({
build: [
{
entry: 'src/main.js',
config: path.join(configRoot, 'vite.main.config.mjs'),
target: 'main',
},
],
renderer: [],
devtron,
});

const mainOutput = (config: {
build?: { rollupOptions?: { output?: unknown } };
}) =>
config.build?.rollupOptions?.output as { banner?: string } | undefined;

it('injects the bootstrap banner into dev main builds when enabled', async () => {
const generator = new ViteConfigGenerator(
devtronForgeConfig(true),
configRoot,
false,
);
const buildConfig = (await generator.getBuildConfigs())[0];
expect(mainOutput(buildConfig)?.banner).toContain('@electron/devtron');
expect(buildConfig.build?.rollupOptions?.external).toContain(
'@electron/devtron',
);
});

it('does not inject the bootstrap into production builds', async () => {
const generator = new ViteConfigGenerator(
devtronForgeConfig(true),
configRoot,
true,
);
const buildConfig = (await generator.getBuildConfigs())[0];
expect(mainOutput(buildConfig)?.banner).toBeUndefined();
expect(buildConfig.build?.rollupOptions?.external).not.toContain(
'@electron/devtron',
);
});

it('does not inject the bootstrap when not enabled', async () => {
const generator = new ViteConfigGenerator(
devtronForgeConfig(),
configRoot,
false,
);
const buildConfig = (await generator.getBuildConfigs())[0];
expect(mainOutput(buildConfig)?.banner).toBeUndefined();
});
});

it('getRendererConfig:renderer', async () => {
const forgeConfig = {
build: [],
Expand Down
12 changes: 12 additions & 0 deletions packages/plugin/vite/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,16 @@ export interface VitePluginConfig {
* @defaultValue `true`
*/
concurrent?: boolean | number;

/**
* Installs the [Devtron](https://github.com/electron/devtron) DevTools
* extension into your app while running `electron-forge start`.
*
* Requires `@electron/devtron` to be installed as a devDependency of your
* app and Electron >= 36. The extension is only injected in development;
* packaged builds are never affected.
*
* @defaultValue false
*/
devtron?: boolean;
}
24 changes: 20 additions & 4 deletions packages/plugin/vite/src/VitePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import { styleText } from 'node:util';

import { readJson, writeJson } from '@electron-forge/core-utils';
import {
canInjectDevtron,
readJson,
writeJson,
} from '@electron-forge/core-utils';
import { namedHookWithTaskFn, PluginBase } from '@electron-forge/plugin-base';
import debug from 'debug';
import { Listr, PRESET_TIMER } from 'listr2';
Expand All @@ -30,7 +34,7 @@ const subprocessWorkerPath = path.resolve(
);

function spawnViteBuild(
pluginConfig: Pick<VitePluginConfig, 'build' | 'renderer'>,
pluginConfig: Pick<VitePluginConfig, 'build' | 'renderer' | 'devtron'>,
kind: 'build' | 'renderer',
index: number,
projectDir: string,
Expand Down Expand Up @@ -80,7 +84,7 @@ function spawnViteBuild(
}

function spawnViteBuildWatch(
pluginConfig: Pick<VitePluginConfig, 'build' | 'renderer'>,
pluginConfig: Pick<VitePluginConfig, 'build' | 'renderer' | 'devtron'>,
index: number,
projectDir: string,
devServerUrls: Record<string, string>,
Expand Down Expand Up @@ -174,6 +178,13 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {

private servers: vite.ViteDevServer[] = [];

/**
* Whether the Devtron bootstrap should be injected into the main process
* bundle. Only ever set during `start`, after verifying the environment
* supports it.
*/
private devtronEnabled = false;

init = (dir: string): void => {
this.setDirectories(dir);

Expand Down Expand Up @@ -209,6 +220,10 @@ export default class VitePlugin extends PluginBase<VitePluginConfig> {
d(`preStart: removing old content from ${this.baseDir}`);
await fs.rm(this.baseDir, { recursive: true, force: true });

if (this.config.devtron) {
this.devtronEnabled = await canInjectDevtron(this.projectDir);
}

return task?.newListr(
[
{
Expand Down Expand Up @@ -340,11 +355,12 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}.`);
*/
private get serializableConfig(): Pick<
VitePluginConfig,
'build' | 'renderer'
'build' | 'renderer' | 'devtron'
> {
return {
build: this.config.build,
renderer: this.config.renderer,
devtron: this.devtronEnabled,
};
}

Expand Down
20 changes: 18 additions & 2 deletions packages/plugin/vite/src/config/vite.main.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getDevtronBootstrapCode } from '@electron-forge/core-utils';
import { type ConfigEnv, mergeConfig, type UserConfig } from 'vite';

import {
Expand All @@ -11,13 +12,28 @@ export function getConfig(
forgeEnv: ConfigEnv<'build'>,
userConfig: UserConfig = {},
): UserConfig {
const { forgeConfigSelf } = forgeEnv;
const { command, forgeConfig, forgeConfigSelf } = forgeEnv;
const define = getBuildDefine(forgeEnv);
// Only inject Devtron into dev builds — `command` is only ever 'serve'
// during `electron-forge start`.
const injectDevtron = Boolean(forgeConfig.devtron) && command === 'serve';
const config: UserConfig = {
build: {
copyPublicDir: false,
rollupOptions: {
external: [...external, 'electron/main'],
external: [
...external,
'electron/main',
...(injectDevtron ? ['@electron/devtron'] : []),
],
...(injectDevtron
? {
// The banner is raw code prepended to the emitted CJS bundle,
// so Rollup never parses it; `@electron/devtron` is resolved
// from the app's node_modules at runtime.
output: { banner: getDevtronBootstrapCode() },
}
: {}),
},
},
plugins: [pluginHotRestart('restart')],
Expand Down
84 changes: 82 additions & 2 deletions packages/plugin/webpack/spec/WebpackConfig.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path';

import { describe, expect, it } from 'vitest';
import { Configuration, Entry } from 'webpack';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import webpackPkg, { Configuration, Entry } from 'webpack';

import {
WebpackConfiguration,
Expand All @@ -13,8 +13,23 @@ import WebpackConfigGenerator, {
ConfigurationFactory,
} from '../src/WebpackConfig';

vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => {
const mod = await importOriginal();
return { ...mod, canInjectDevtron: vi.fn(async () => true) };
});

const { canInjectDevtron } = await import('@electron-forge/core-utils');

const mockProjectDir = process.platform === 'win32' ? 'C:\\path' : '/path';

function hasDevtronBannerPlugin(
plugins?: Required<Configuration>['plugins'],
): boolean {
return (plugins || []).some(
(plugin) => plugin instanceof webpackPkg.BannerPlugin,
);
}

function hasAssetRelocatorPatchPlugin(
plugins?: Required<Configuration>['plugins'],
): boolean {
Expand Down Expand Up @@ -298,6 +313,71 @@ describe('WebpackConfigGenerator', () => {
);
});

describe('devtron', () => {
beforeEach(() => {
vi.mocked(canInjectDevtron).mockClear();
vi.mocked(canInjectDevtron).mockResolvedValue(true);
});

const devtronConfig = (devtron?: boolean) =>
({
mainConfig: {
entry: 'main.js',
},
renderer: {
entryPoints: [] as WebpackPluginEntryPoint[],
},
devtron,
}) as WebpackPluginConfig;

it('injects the bootstrap banner in development when enabled', async () => {
const generator = new WebpackConfigGenerator(
devtronConfig(true),
mockProjectDir,
false,
3000,
);
const webpackConfig = await generator.getMainConfig();
expect(hasDevtronBannerPlugin(webpackConfig.plugins)).toEqual(true);
});

it('does not inject the bootstrap in production', async () => {
const generator = new WebpackConfigGenerator(
devtronConfig(true),
mockProjectDir,
true,
3000,
);
const webpackConfig = await generator.getMainConfig();
expect(hasDevtronBannerPlugin(webpackConfig.plugins)).toEqual(false);
expect(canInjectDevtron).not.toHaveBeenCalled();
});

it('does not inject the bootstrap when not enabled', async () => {
const generator = new WebpackConfigGenerator(
devtronConfig(),
mockProjectDir,
false,
3000,
);
const webpackConfig = await generator.getMainConfig();
expect(hasDevtronBannerPlugin(webpackConfig.plugins)).toEqual(false);
expect(canInjectDevtron).not.toHaveBeenCalled();
});

it('does not inject the bootstrap when the environment does not support it', async () => {
vi.mocked(canInjectDevtron).mockResolvedValue(false);
const generator = new WebpackConfigGenerator(
devtronConfig(true),
mockProjectDir,
false,
3000,
);
const webpackConfig = await generator.getMainConfig();
expect(hasDevtronBannerPlugin(webpackConfig.plugins)).toEqual(false);
});
});

it('generates a config with a relative entry path', async () => {
const config = {
mainConfig: {
Expand Down
11 changes: 11 additions & 0 deletions packages/plugin/webpack/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,17 @@ export interface WebpackPluginConfig {
WebpackDevServer.Configuration,
'port' | 'static' | 'setupExitSignals' | 'Content-Security-Policy'
>;
/**
* Installs the [Devtron](https://github.com/electron/devtron) DevTools
* extension into your app while running `electron-forge start`.
*
* Requires `@electron/devtron` to be installed as a devDependency of your
* app and Electron >= 36. The extension is only injected in development;
* packaged builds are never affected.
*
* @defaultValue false
*/
devtron?: boolean;
}

export type WebpackConfiguration =
Expand Down
28 changes: 26 additions & 2 deletions packages/plugin/webpack/src/WebpackConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import HtmlWebpackPlugin from 'html-webpack-plugin';
import type * as webpack from 'webpack';
import webpackPkg from 'webpack';

const { DefinePlugin, ExternalsPlugin } = webpackPkg;
const { BannerPlugin, DefinePlugin, ExternalsPlugin } = webpackPkg;
import {
canInjectDevtron,
getDevtronBootstrapCode,
} from '@electron-forge/core-utils';
import { merge as webpackMerge } from 'webpack-merge';

import {
Expand Down Expand Up @@ -223,6 +227,26 @@ export default class WebpackConfigGenerator {
};
mainConfig.entry = fix(mainConfig.entry as EntryType);

const plugins: webpack.WebpackPluginInstance[] = [
new DefinePlugin(this.getDefines()),
];
if (
!this.isProd &&
this.pluginConfig.devtron &&
(await canInjectDevtron(this.projectDir))
) {
// The banner is raw code prepended to the emitted bundle, so webpack
// never parses it; `@electron/devtron` is resolved from the app's
// node_modules at runtime instead of being bundled.
plugins.push(
new BannerPlugin({
banner: getDevtronBootstrapCode(),
raw: true,
entryOnly: true,
}),
);
}

return webpackMerge(
{
devtool: 'source-map',
Expand All @@ -233,7 +257,7 @@ export default class WebpackConfigGenerator {
filename: 'index.js',
libraryTarget: 'commonjs2',
},
plugins: [new DefinePlugin(this.getDefines())],
plugins,
node: {
__dirname: false,
__filename: false,
Expand Down
Loading
Loading