diff --git a/packages/api/cli/src/electron-forge-init.ts b/packages/api/cli/src/electron-forge-init.ts index 29c3ee86cd..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,6 +89,8 @@ program } } + const packageManager: string = await getPackageManager(); + const bundler: string = await prompt.run>( select, { @@ -115,6 +133,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/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 660ccbf916..8ac5287c20 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. @@ -85,7 +86,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( @@ -97,10 +100,30 @@ export const resolvePackageManager: () => Promise = async () => { lockfilePM = PM_FROM_LOCKFILE[lockfileName]; } - let installer; - let installerVersion; + let installer: string | undefined; + let installerVersion: string | undefined; - if (typeof process.env.NODE_INSTALLER === 'string') { + if (packageManager) { + if (explicitPMCache && explicitPMCache.executable === packageManager) { + d(`Using cached explicit package manager: ${explicitPMCache.executable}`); + return explicitPMCache; + } + + 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 (!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( @@ -164,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; +}