From d4c89ef1ca05eb26fa5fb72896826a6c40cc41f7 Mon Sep 17 00:00:00 2001 From: youaresoyoung Date: Fri, 12 Sep 2025 11:13:22 -0700 Subject: [PATCH 1/4] feat(init): add explicit package manager selection (npm/yarn/pnpm) to forge init --- packages/api/cli/src/electron-forge-init.ts | 21 +++++++++++++++++++ packages/api/core/src/api/init.ts | 7 ++++++- .../utils/core-utils/src/package-manager.ts | 7 ++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/api/cli/src/electron-forge-init.ts b/packages/api/cli/src/electron-forge-init.ts index 29c3ee86cd..6c0e134ee9 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/api/cli/src/electron-forge-init.ts @@ -73,6 +73,26 @@ program } } + const packageManager: string = await prompt.run< + Prompt + >(select, { + message: 'Select a package manager', + choices: [ + { + name: 'npm', + value: 'npm', + }, + { + name: 'Yarn', + value: 'yarn', + }, + { + name: 'pnpm', + value: 'pnpm', + }, + ], + }); + const bundler: string = await prompt.run>( select, { @@ -115,6 +135,7 @@ program ); } + initOpts.packageManager = packageManager; initOpts.template = `${bundler}${language ? `-${language}` : ''}`; initOpts.skipGit = !(await prompt.run(confirm, { message: `Would you like to initialize Git in your new project?`, diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index eb532f4c57..49ca3d32d3 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -46,6 +46,10 @@ export interface InitOptions { * By default, Forge initializes a git repository in the project directory. Set this option to `true` to skip this step. */ skipGit?: boolean; + /** + * Force a package manager to use (npm|yarn|pnpm). Internally sets NODE_INSTALLER (deprecated upstream) to ensure template PM-specific logic runs. + */ + packageManager?: string; } async function validateTemplate( @@ -75,6 +79,7 @@ export default async ({ force = false, template = 'base', skipGit = false, + packageManager, }: InitOptions): Promise => { d(`Initializing in: ${dir}`); @@ -86,7 +91,7 @@ export default async ({ { title: `Resolving package manager`, task: async (ctx, task) => { - ctx.pm = await resolvePackageManager(); + ctx.pm = await resolvePackageManager(packageManager); task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)}`; }, }, diff --git a/packages/utils/core-utils/src/package-manager.ts b/packages/utils/core-utils/src/package-manager.ts index 660ccbf916..c055e2270b 100644 --- a/packages/utils/core-utils/src/package-manager.ts +++ b/packages/utils/core-utils/src/package-manager.ts @@ -85,7 +85,9 @@ function pmFromUserAgent() { * Supported package managers are `yarn`, `pnpm`, and `npm`. * */ -export const resolvePackageManager: () => Promise = async () => { +export const resolvePackageManager: ( + packageManager?: string, +) => Promise = async (packageManager) => { const executingPM = pmFromUserAgent(); let lockfilePM; const lockfile = await findUp( @@ -96,6 +98,9 @@ export const resolvePackageManager: () => Promise = async () => { const lockfileName = path.basename(lockfile); lockfilePM = PM_FROM_LOCKFILE[lockfileName]; } + if (packageManager) { + process.env.NODE_INSTALLER = packageManager; + } let installer; let installerVersion; From 68c69eb95285f8943aab297b258e9e6cc3665d4e Mon Sep 17 00:00:00 2001 From: youaresoyoung Date: Fri, 12 Sep 2025 12:08:34 -0700 Subject: [PATCH 2/4] feat(core-utils): cache explicit package manager selection and skip env/lockfile fallback --- .../utils/core-utils/src/package-manager.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/utils/core-utils/src/package-manager.ts b/packages/utils/core-utils/src/package-manager.ts index c055e2270b..681e5ed783 100644 --- a/packages/utils/core-utils/src/package-manager.ts +++ b/packages/utils/core-utils/src/package-manager.ts @@ -22,6 +22,7 @@ export type PMDetails = { }; let hasWarned = false; +let explicitPMCache: PMDetails | undefined; /** * Supported package managers and the commands and flags they need to install dependencies. @@ -98,14 +99,31 @@ export const resolvePackageManager: ( const lockfileName = path.basename(lockfile); lockfilePM = PM_FROM_LOCKFILE[lockfileName]; } + + let installer: string | undefined; + let installerVersion: string | undefined; + if (packageManager) { - process.env.NODE_INSTALLER = packageManager; - } + if (explicitPMCache && explicitPMCache.executable === packageManager) { + d(`Using cached explicit package manager: ${explicitPMCache.executable}`); + return explicitPMCache; + } - let installer; - let installerVersion; + if (Object.keys(PACKAGE_MANAGERS).includes(packageManager)) { + const pm = PACKAGE_MANAGERS[packageManager as SupportedPackageManager]; + installerVersion = await spawnPackageManager(pm, ['--version']); + explicitPMCache = { ...pm, version: installerVersion }; + d(`Resolved and cached explicit package manager: ${pm.executable}`); + return explicitPMCache; + } + } - if (typeof process.env.NODE_INSTALLER === 'string') { + if (!packageManager && explicitPMCache) { + d( + `Returning previously cached explicit package manager: ${explicitPMCache.executable}`, + ); + return explicitPMCache; + } else if (typeof process.env.NODE_INSTALLER === 'string') { if (Object.keys(PACKAGE_MANAGERS).includes(process.env.NODE_INSTALLER)) { installer = process.env.NODE_INSTALLER; installerVersion = await spawnPackageManager( From 9e5318aa1dd9f8a72cdc638a6ba423a34209a10d Mon Sep 17 00:00:00 2001 From: youaresoyoung Date: Fri, 12 Sep 2025 12:29:13 -0700 Subject: [PATCH 3/4] test(core-utils): add reset helper and specs for explicit package manager caching --- .../core-utils/spec/package-manager.spec.ts | 33 +++++++++++++++++++ .../utils/core-utils/src/package-manager.ts | 5 +++ 2 files changed, 38 insertions(+) diff --git a/packages/utils/core-utils/spec/package-manager.spec.ts b/packages/utils/core-utils/spec/package-manager.spec.ts index 408ee7ae6d..bb89a76b4a 100644 --- a/packages/utils/core-utils/spec/package-manager.spec.ts +++ b/packages/utils/core-utils/spec/package-manager.spec.ts @@ -3,6 +3,7 @@ import findUp from 'find-up'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { + __resetExplicitPMCacheForTests, resolvePackageManager, spawnPackageManager, } from '../src/package-manager'; @@ -174,4 +175,36 @@ describe('package-manager', () => { expect(result).toBe('foo'); }); }); + + describe('explicit argument caching', () => { + beforeEach(() => { + __resetExplicitPMCacheForTests(); + delete process.env.NODE_INSTALLER; + delete process.env.npm_config_user_agent; + }); + + it('should cache explicit argument and ignore later env / lockfile', async () => { + vi.mocked(spawn).mockResolvedValueOnce('10.0.0'); + const first = await resolvePackageManager('pnpm'); + expect(first.executable).toBe('pnpm'); + expect(first.version).toBe('10.0.0'); + + process.env.NODE_INSTALLER = 'yarn'; + vi.mocked(spawn).mockResolvedValue('9.9.9'); + const second = await resolvePackageManager(); + expect(second.executable).toBe('pnpm'); + expect(second.version).toBe('10.0.0'); + }); + + it('should fallback to npm and cache when explicit argument unsupported', async () => { + vi.mocked(spawn).mockResolvedValue('9.99.99'); + const result = await resolvePackageManager('good coffee'); + expect(result.executable).toBe('npm'); + expect(result.version).toBe('9.99.99'); + + const again = await resolvePackageManager(); + expect(again.executable).toBe('npm'); + expect(again.version).toBe('9.99.99'); + }); + }); }); diff --git a/packages/utils/core-utils/src/package-manager.ts b/packages/utils/core-utils/src/package-manager.ts index 681e5ed783..8ac5287c20 100644 --- a/packages/utils/core-utils/src/package-manager.ts +++ b/packages/utils/core-utils/src/package-manager.ts @@ -187,3 +187,8 @@ export const spawnPackageManager = async ( ): Promise => { return (await spawn(pm.executable, args, opts)).trim(); }; + +// Test-only helper to clear the explicit package manager cache between specs. +export function __resetExplicitPMCacheForTests() { + explicitPMCache = undefined; +} From 23809ecb84d343c2a9454a8028c3731e6334067a Mon Sep 17 00:00:00 2001 From: youaresoyoung Date: Fri, 12 Sep 2025 13:39:51 -0700 Subject: [PATCH 4/4] feat(cli): unify package manager prompt via helper across init paths --- packages/api/cli/src/electron-forge-init.ts | 38 ++++++++++----------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/api/cli/src/electron-forge-init.ts b/packages/api/cli/src/electron-forge-init.ts index 6c0e134ee9..85b73aa841 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/api/cli/src/electron-forge-init.ts @@ -45,12 +45,28 @@ program }, { task: async (initOpts, task): Promise => { - // only run interactive prompts if no args passed and not in CI environment + // If any CLI flags are provided, run only the minimal prompt (package manager). + // Otherwise run full interactive initialization. + const getPackageManager = async () => { + const prompt = task.prompt(ListrInquirerPromptAdapter); + + const pm: string = await prompt.run>(select, { + message: 'Select a package manager', + choices: [ + { name: 'npm', value: 'npm' }, + { name: 'Yarn', value: 'yarn' }, + { name: 'pnpm', value: 'pnpm' }, + ], + }); + return pm; + }; + if ( Object.keys(options).length > 0 || process.env.CI || !process.stdout.isTTY ) { + initOpts.packageManager = await getPackageManager(); return; } @@ -73,25 +89,7 @@ program } } - const packageManager: string = await prompt.run< - Prompt - >(select, { - message: 'Select a package manager', - choices: [ - { - name: 'npm', - value: 'npm', - }, - { - name: 'Yarn', - value: 'yarn', - }, - { - name: 'pnpm', - value: 'pnpm', - }, - ], - }); + const packageManager: string = await getPackageManager(); const bundler: string = await prompt.run>( select,