From 1b049180643d25781afed4a638beb62068290b7e Mon Sep 17 00:00:00 2001 From: bgl gwyng Date: Thu, 12 Feb 2026 11:34:54 +0900 Subject: [PATCH] feat: add `electron-forge bundle` CLI command Add a standalone `bundle` command that runs only the bundling step (generateAssets + prePackage hooks) without invoking @electron/packager. This ensures the same webpack/vite config is used as `package` while allowing users to produce bundle output independently. Co-Authored-By: Claude Opus 4.6 --- packages/api/cli/src/electron-forge-bundle.ts | 35 ++++ packages/api/cli/src/electron-forge.ts | 4 + packages/api/core/src/api/bundle.ts | 170 ++++++++++++++++++ packages/api/core/src/api/index.ts | 11 ++ 4 files changed, 220 insertions(+) create mode 100644 packages/api/cli/src/electron-forge-bundle.ts create mode 100644 packages/api/core/src/api/bundle.ts diff --git a/packages/api/cli/src/electron-forge-bundle.ts b/packages/api/cli/src/electron-forge-bundle.ts new file mode 100644 index 0000000000..f95bf1e2c7 --- /dev/null +++ b/packages/api/cli/src/electron-forge-bundle.ts @@ -0,0 +1,35 @@ +import { initializeProxy } from '@electron/get'; +import { api, BundleOptions } from '@electron-forge/core'; +import { program } from 'commander'; + +import './util/terminate'; +import packageJSON from '../package.json'; + +import { resolveWorkingDir } from './util/resolve-working-dir'; + +program + .version(packageJSON.version, '-V, --version', 'Output the current version') + .helpOption('-h, --help', 'Output usage information') + .argument( + '[dir]', + 'Directory to run the command in. (default: current directory)', + ) + .option('-a, --arch [arch]', 'Target build architecture') + .option('-p, --platform [platform]', 'Target build platform') + .action(async (dir) => { + const workingDir = resolveWorkingDir(dir); + + const options = program.opts(); + + initializeProxy(); + + const bundleOpts: BundleOptions = { + dir: workingDir, + interactive: true, + }; + if (options.arch) bundleOpts.arch = options.arch; + if (options.platform) bundleOpts.platform = options.platform; + + await api.bundle(bundleOpts); + }) + .parse(process.argv); diff --git a/packages/api/cli/src/electron-forge.ts b/packages/api/cli/src/electron-forge.ts index 89a0f1d9e1..34edd3bfe5 100755 --- a/packages/api/cli/src/electron-forge.ts +++ b/packages/api/cli/src/electron-forge.ts @@ -32,6 +32,10 @@ program 'Start the current Electron application in development mode.', ) .command('package', 'Package the current Electron application.') + .command( + 'bundle', + 'Bundle the current Electron application source code for production.', + ) .command( 'make', 'Generate distributables for the current Electron application.', diff --git a/packages/api/core/src/api/bundle.ts b/packages/api/core/src/api/bundle.ts new file mode 100644 index 0000000000..1d98658987 --- /dev/null +++ b/packages/api/core/src/api/bundle.ts @@ -0,0 +1,170 @@ +import { getHostArch } from '@electron/get'; +import { + ForgeArch, + ForgeListrTaskFn, + ForgePlatform, + ResolvedForgeConfig, +} from '@electron-forge/shared-types'; +import { autoTrace, delayTraceTillSignal } from '@electron-forge/tracer'; +import chalk from 'chalk'; +import debug from 'debug'; +import { Listr } from 'listr2'; + +import getForgeConfig from '../util/forge-config'; +import { getHookListrTasks } from '../util/hook'; +import { readMutatedPackageJson } from '../util/read-package-json'; +import resolveDir from '../util/resolve-dir'; + +const d = debug('electron-forge:bundle'); + +type BundleContext = { + dir: string; + forgeConfig: ResolvedForgeConfig; + packageJSON: any; +}; + +export interface BundleOptions { + /** + * The path to the app to bundle + */ + dir?: string; + /** + * Whether to use sensible defaults or prompt the user visually + */ + interactive?: boolean; + /** + * The target arch + */ + arch?: ForgeArch; + /** + * The target platform + */ + platform?: ForgePlatform; +} + +export const listrBundle = ( + childTrace: typeof autoTrace, + { + dir: providedDir = process.cwd(), + interactive = false, + arch = getHostArch() as ForgeArch, + platform = process.platform as ForgePlatform, + }: BundleOptions, +) => { + d('bundling with options', { providedDir, interactive, arch, platform }); + + const runner = new Listr( + [ + { + title: 'Preparing to bundle application', + task: childTrace>>( + { name: 'bundle-prepare', category: '@electron-forge/core' }, + async (_, ctx) => { + const resolvedDir = await resolveDir(providedDir); + if (!resolvedDir) { + throw new Error( + 'Failed to locate compilable Electron application', + ); + } + ctx.dir = resolvedDir; + + ctx.forgeConfig = await getForgeConfig(resolvedDir); + ctx.packageJSON = await readMutatedPackageJson( + resolvedDir, + ctx.forgeConfig, + ); + + if (!ctx.packageJSON.main) { + throw new Error( + 'packageJSON.main must be set to a valid entry point for your Electron app', + ); + } + }, + ), + }, + { + title: 'Running bundling hooks', + task: childTrace>>( + { name: 'run-bundling-hooks', category: '@electron-forge/core' }, + async (childTrace, { forgeConfig }, task) => { + return delayTraceTillSignal( + childTrace, + task.newListr([ + { + title: `Running ${chalk.yellow('generateAssets')} hook`, + task: childTrace>( + { + name: 'run-generateAssets-hook', + category: '@electron-forge/core', + }, + async (childTrace, _, task) => { + return delayTraceTillSignal( + childTrace, + task.newListr( + await getHookListrTasks( + childTrace, + forgeConfig, + 'generateAssets', + platform, + arch, + ), + ), + 'run', + ); + }, + ), + }, + { + title: `Running ${chalk.yellow('prePackage')} hook`, + task: childTrace>( + { + name: 'run-prePackage-hook', + category: '@electron-forge/core', + }, + async (childTrace, _, task) => { + return delayTraceTillSignal( + childTrace, + task.newListr( + await getHookListrTasks( + childTrace, + forgeConfig, + 'prePackage', + platform, + arch, + ), + ), + 'run', + ); + }, + ), + }, + ]), + 'run', + ); + }, + ), + }, + ], + { + concurrent: false, + silentRendererCondition: !interactive, + fallbackRendererCondition: + Boolean(process.env.DEBUG) || Boolean(process.env.CI), + rendererOptions: { + collapseSubtasks: false, + collapseErrors: false, + }, + ctx: {} as BundleContext, + }, + ); + + return runner; +}; + +export default autoTrace( + { name: 'bundle()', category: '@electron-forge/core' }, + async (childTrace, opts: BundleOptions): Promise => { + const runner = listrBundle(childTrace, opts); + await runner.run(); + }, +); diff --git a/packages/api/core/src/api/index.ts b/packages/api/core/src/api/index.ts index 5a358595ac..2215395e36 100644 --- a/packages/api/core/src/api/index.ts +++ b/packages/api/core/src/api/index.ts @@ -3,6 +3,7 @@ import { ElectronProcess, ForgeMakeResult } from '@electron-forge/shared-types'; // eslint-disable-next-line n/no-missing-import import ForgeUtils from '../util'; +import bundle, { BundleOptions } from './bundle'; import _import, { ImportOptions } from './import'; import init, { InitOptions } from './init'; import make, { MakeOptions } from './make'; @@ -11,6 +12,15 @@ import publish, { PublishOptions } from './publish'; import start, { StartOptions } from './start'; export class ForgeAPI { + /** + * Bundle the current Electron application source code using the configured + * bundler plugin (e.g. webpack, vite). Runs generateAssets and prePackage + * hooks without running @electron/packager. + */ + bundle(opts: BundleOptions): Promise { + return bundle(opts); + } + /** * Attempt to import a given module directory to the Electron Forge standard. * @@ -63,6 +73,7 @@ const api = new ForgeAPI(); const utils = new ForgeUtils(); export { + BundleOptions, ForgeMakeResult, ElectronProcess, ForgeUtils,