From 2a3b7506a0660f3d69c8bb95818e6e2449a44de7 Mon Sep 17 00:00:00 2001 From: youaresoyoung Date: Fri, 12 Sep 2025 11:13:22 -0700 Subject: [PATCH 01/19] 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 f25ac25739..c4c2a0ac8d 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/api/cli/src/electron-forge-init.ts @@ -79,6 +79,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, { @@ -121,6 +141,7 @@ program ); } + initOpts.packageManager = packageManager; initOpts.template = `${bundler}${language ? `-${language}` : ''}`; // TODO: add prompt for passing in an exact version as well diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index cafcab0bc8..d1f6a6e357 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -54,6 +54,10 @@ export interface InitOptions { * @defaultValue The `latest` tag on npm. */ electronVersion?: string; + /** + * 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( @@ -84,6 +88,7 @@ export default async ({ template = 'base', skipGit = false, electronVersion = 'latest', + packageManager, }: InitOptions): Promise => { d(`Initializing in: ${dir}`); @@ -96,7 +101,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)} v${ctx.pm.version}`; }, }, 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 8cb13232e4d1c86df588ceb8c7380e4abf5e3d88 Mon Sep 17 00:00:00 2001 From: youaresoyoung Date: Fri, 12 Sep 2025 12:08:34 -0700 Subject: [PATCH 02/19] 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 994559835acda4ff4bf8be3ac9cf59a70d293263 Mon Sep 17 00:00:00 2001 From: youaresoyoung Date: Fri, 12 Sep 2025 12:29:13 -0700 Subject: [PATCH 03/19] 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 556a5da5587a734dcc2e786d1cfdd8e9d1fc9665 Mon Sep 17 00:00:00 2001 From: youaresoyoung Date: Fri, 12 Sep 2025 13:39:51 -0700 Subject: [PATCH 04/19] 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 c4c2a0ac8d..b197e17d04 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/api/cli/src/electron-forge-init.ts @@ -50,12 +50,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; } @@ -79,25 +95,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, From 09fdf0a1b91304626511a70b2315d22f05f7d85f Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 18 Dec 2025 17:30:10 -0800 Subject: [PATCH 05/19] feat(init): add `--package-manager` flag --- packages/api/cli/src/electron-forge-init.ts | 34 +- packages/api/core/src/api/init.ts | 18 +- packages/template/base/src/BaseTemplate.ts | 9 +- .../core-utils/spec/package-manager.spec.ts | 327 ++++++++++-------- .../utils/core-utils/src/package-manager.ts | 59 ++-- 5 files changed, 244 insertions(+), 203 deletions(-) diff --git a/packages/api/cli/src/electron-forge-init.ts b/packages/api/cli/src/electron-forge-init.ts index b197e17d04..a3dadb6e27 100644 --- a/packages/api/cli/src/electron-forge-init.ts +++ b/packages/api/cli/src/electron-forge-init.ts @@ -33,6 +33,10 @@ program '--electron-version [version]', 'Set a specific Electron version for your Forge project. Can take in a version string (e.g. `38.3.0`) or `latest`, `beta`, or `nightly` tags.', ) + .option( + '--package-manager [name]', + 'Set a specific package manager to use for your Forge project. Supported package managers are `npm`, `pnpm`, and `yarn`. You can also specify an exact version to use (e.g. `yarn@1.22.22`).', + ) .action(async (dir) => { const options = program.opts(); const tasks = new Listr( @@ -46,32 +50,16 @@ program initOpts.skipGit = Boolean(options.skipGit); initOpts.dir = resolveWorkingDir(dir, false); initOpts.electronVersion = options.electronVersion ?? 'latest'; + initOpts.packageManager = options.packageManager ?? 'npm@latest'; }, }, { task: async (initOpts, task): Promise => { - // 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; } @@ -95,7 +83,17 @@ program } } - const packageManager: string = await getPackageManager(); + const packageManager: string = await prompt.run< + Prompt + >(select, { + message: 'Select a package manager', + choices: [ + { name: 'npm', value: 'npm@latest' }, + { name: 'pnpm', value: 'pnpm@latest' }, + { name: 'Yarn (Berry)', value: 'yarn@latest' }, + { name: 'Yarn (Classic)', value: 'yarn@1' }, + ], + }); const bundler: string = await prompt.run>( select, diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index d1f6a6e357..245221ce30 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -2,6 +2,7 @@ import path from 'node:path'; import { PMDetails, resolvePackageManager } from '@electron-forge/core-utils'; import { ForgeTemplate } from '@electron-forge/shared-types'; +import { spawn } from '@malept/cross-spawn-promise'; import chalk from 'chalk'; import debug from 'debug'; import { Listr } from 'listr2'; @@ -102,7 +103,7 @@ export default async ({ title: `Resolving package manager`, task: async (ctx, task) => { ctx.pm = await resolvePackageManager(packageManager); - task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)} v${ctx.pm.version}`; + task.title = `Resolved package manager: ${chalk.cyan(`${ctx.pm.executable}@${ctx.pm.version}`)}`; }, }, { @@ -174,6 +175,21 @@ export default async ({ } }, }, + { + title: `Setting package manager with Corepack`, + task: async ({ pm }, task) => { + const pmString = `${pm.executable}@${pm.version}`; + try { + await spawn('corepack', ['use', pmString], { + cwd: dir, + }); + task.title = `Set ${chalk.cyan(pmString)} via Corepack`; + } catch (e) { + d('corepack failed to run with error', e); + task.title = `Forge was unable to set ${chalk.cyan(pmString)} via Corepack and will fall back to ${chalk.cyan('npm')}. If you are using Node.js >= 25, you will need to install corepack via ${chalk.green('npm install -g corepack')}. Otherwise, you may need to enable Corepack shims via ${chalk.green('corepack enable')}.`; + } + }, + }, { title: 'Installing template dependencies', task: async ({ templateModule }, task) => { diff --git a/packages/template/base/src/BaseTemplate.ts b/packages/template/base/src/BaseTemplate.ts index 5bc5ad32af..61af6e3a01 100644 --- a/packages/template/base/src/BaseTemplate.ts +++ b/packages/template/base/src/BaseTemplate.ts @@ -76,7 +76,7 @@ export class BaseTemplate implements ForgeTemplate { // Support Yarn 2+ by default by initializing with nodeLinker: node-modules pm.executable === 'yarn' && pm.version && - semver.gte(pm.version, '2.0.0') + (pm.version === 'latest' || semver.gte(pm.version, '2.0.0')) ) { rootFiles.push('_yarnrc.yml'); } @@ -145,13 +145,6 @@ export class BaseTemplate implements ForgeTemplate { packageJSON.pnpm = { onlyBuiltDependencies: ['electron', 'electron-winstaller'], }; - } else if ( - pm.executable === 'yarn' && - typeof pm.version === 'string' && - semver.gte(pm.version, '2.0.0') - ) { - d('Detected Yarn 2+, adding packageManager field to package.json'); - packageJSON.packageManager = `yarn@${pm.version}`; } packageJSON.scripts.lint = 'echo "No linting configured"'; diff --git a/packages/utils/core-utils/spec/package-manager.spec.ts b/packages/utils/core-utils/spec/package-manager.spec.ts index bb89a76b4a..3dc42f9929 100644 --- a/packages/utils/core-utils/spec/package-manager.spec.ts +++ b/packages/utils/core-utils/spec/package-manager.spec.ts @@ -25,142 +25,205 @@ describe('package-manager', () => { process.env.npm_config_user_agent = originalUa; }; }); - describe('npm_config_user_agent', () => { - it.each([ - { - ua: 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64', - pm: 'yarn', - version: '1.22.22', - }, - { - ua: 'pnpm/10.0.0 npm/? node/v20.11.1 darwin arm64', - pm: 'pnpm', - version: '10.0.0', - }, - { - ua: 'npm/10.9.2 node/v22.13.0 darwin arm64 workspaces/false', - pm: 'npm', - version: '10.9.2', - }, - ])('with $ua', async ({ ua, pm, version }) => { - process.env.npm_config_user_agent = ua; - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - pm, - ); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'version', - version, - ); - }); + describe('resolvePackageManager', () => { + describe('npm_config_user_agent', () => { + it.each([ + { + ua: 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64', + pm: 'yarn', + version: '1.22.22', + }, + { + ua: 'pnpm/10.0.0 npm/? node/v20.11.1 darwin arm64', + pm: 'pnpm', + version: '10.0.0', + }, + { + ua: 'npm/10.9.2 node/v22.13.0 darwin arm64 workspaces/false', + pm: 'npm', + version: '10.9.2', + }, + ])('with $ua', async ({ ua, pm, version }) => { + process.env.npm_config_user_agent = ua; + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + pm, + ); + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'version', + version, + ); + }); - it('should return yarn if npm_config_user_agent=yarn', async () => { - process.env.npm_config_user_agent = - 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'yarn', - ); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'version', - '1.22.22', - ); - }); + it('should return yarn if npm_config_user_agent=yarn', async () => { + process.env.npm_config_user_agent = + 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + 'yarn', + ); + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'version', + '1.22.22', + ); + }); - it('should return pnpm if npm_config_user_agent=pnpm', async () => { - process.env.npm_config_user_agent = - 'pnpm/10.0.0 npm/? node/v20.11.1 darwin arm64'; - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'pnpm', - ); - }); + it('should return pnpm if npm_config_user_agent=pnpm', async () => { + process.env.npm_config_user_agent = + 'pnpm/10.0.0 npm/? node/v20.11.1 darwin arm64'; + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + 'pnpm', + ); + }); - it('should return npm if npm_config_user_agent=npm', async () => { - process.env.npm_config_user_agent = - 'npm/10.9.2 node/v22.13.0 darwin arm64 workspaces/false'; - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'npm', - ); + it('should return npm if npm_config_user_agent=npm', async () => { + process.env.npm_config_user_agent = + 'npm/10.9.2 node/v22.13.0 darwin arm64 workspaces/false'; + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + 'npm', + ); + }); }); - }); - describe('NODE_INSTALLER', () => { - let initialNodeInstallerValue: string | undefined; - - beforeEach(() => { - initialNodeInstallerValue = process.env.NODE_INSTALLER; - delete process.env.NODE_INSTALLER; - // NODE_INSTALLER is deprecated for Electron Forge 8 and throws a console.warn that we want to silence in tests - vi.spyOn(console, 'warn').mockImplementation(() => undefined); - - return () => { - // For cleanup, we want to restore process.env.NODE_INSTALLER. - // If it wasn't explicitly set before, we delete the value set during the test. - // Otherwise, we restore the initial value. - if (!initialNodeInstallerValue) { - delete process.env.NODE_INSTALLER; - } else { - process.env.NODE_INSTALLER = initialNodeInstallerValue; - } - vi.restoreAllMocks(); - }; - }); + describe('NODE_INSTALLER', () => { + let initialNodeInstallerValue: string | undefined; + + beforeEach(() => { + initialNodeInstallerValue = process.env.NODE_INSTALLER; + delete process.env.NODE_INSTALLER; + // NODE_INSTALLER is deprecated for Electron Forge 8 and throws a console.warn that we want to silence in tests + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + return () => { + // For cleanup, we want to restore process.env.NODE_INSTALLER. + // If it wasn't explicitly set before, we delete the value set during the test. + // Otherwise, we restore the initial value. + if (!initialNodeInstallerValue) { + delete process.env.NODE_INSTALLER; + } else { + process.env.NODE_INSTALLER = initialNodeInstallerValue; + } + vi.restoreAllMocks(); + }; + }); - it.each([{ pm: 'yarn' }, { pm: 'npm' }, { pm: 'pnpm' }])( - 'should return $pm if NODE_INSTALLER=$pm', - async ({ pm }) => { - process.env.NODE_INSTALLER = pm; - vi.mocked(spawn).mockResolvedValue('9.9.9'); + it.each([{ pm: 'yarn' }, { pm: 'npm' }, { pm: 'pnpm' }])( + 'should return $pm if NODE_INSTALLER=$pm', + async ({ pm }) => { + process.env.NODE_INSTALLER = pm; + vi.mocked(spawn).mockResolvedValue('9.9.9'); + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + pm, + ); + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'version', + '9.9.9', + ); + }, + ); + + it('should return npm if package manager is unsupported', async () => { + process.env.NODE_INSTALLER = 'bun'; + console.warn = vi.fn(); + vi.mocked(spawn).mockResolvedValue('1.22.22'); await expect(resolvePackageManager()).resolves.toHaveProperty( 'executable', - pm, + 'npm', ); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'version', - '9.9.9', + expect(console.warn).toHaveBeenCalledWith( + '⚠', + expect.stringContaining('Package manager bun is unsupported'), ); - }, - ); + }); + }); - it('should return npm if package manager is unsupported', async () => { - process.env.NODE_INSTALLER = 'bun'; - console.warn = vi.fn(); + it('should use the package manager for the nearest ancestor lockfile if detected', async () => { + delete process.env.npm_config_user_agent; + vi.mocked(findUp).mockResolvedValue('/Users/foo/bar/yarn.lock'); vi.mocked(spawn).mockResolvedValue('1.22.22'); + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'executable', + 'yarn', + ); + }); + + it('should fall back to npm if no other strategy worked', async () => { + delete process.env.npm_config_user_agent; + vi.mocked(findUp).mockResolvedValue(undefined); + vi.mocked(spawn).mockResolvedValue('9.99.99'); await expect(resolvePackageManager()).resolves.toHaveProperty( 'executable', 'npm', ); - expect(console.warn).toHaveBeenCalledWith( - '⚠', - expect.stringContaining('Package manager bun is unsupported'), + await expect(resolvePackageManager()).resolves.toHaveProperty( + 'version', + '9.99.99', ); }); - }); - it('should use the package manager for the nearest ancestor lockfile if detected', async () => { - delete process.env.npm_config_user_agent; - vi.mocked(findUp).mockResolvedValue('/Users/foo/bar/yarn.lock'); - vi.mocked(spawn).mockResolvedValue('1.22.22'); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'yarn', - ); - }); + describe('with an explicit package manager passed in', () => { + beforeEach(() => { + __resetExplicitPMCacheForTests(); + delete process.env.NODE_INSTALLER; + delete process.env.npm_config_user_agent; + }); - it('should fall back to npm if no other strategy worked', async () => { - delete process.env.npm_config_user_agent; - vi.mocked(findUp).mockResolvedValue(undefined); - vi.mocked(spawn).mockResolvedValue('9.99.99'); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'npm', - ); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'version', - '9.99.99', - ); + it('should accept a string with only the package manager anme', async () => { + const first = await resolvePackageManager('pnpm'); + expect(first.executable).toBe('pnpm'); + expect(first.version).toBe('latest'); + }); + + it('should accept the @latest tag', async () => { + const first = await resolvePackageManager('yarn@latest'); + expect(first.executable).toBe('yarn'); + expect(first.version).toBe('latest'); + }); + + it('should accept a full version', async () => { + const first = await resolvePackageManager('yarn@1.22.22'); + expect(first.executable).toBe('yarn'); + expect(first.version).toBe('1.22.22'); + }); + + it('should accept a major.minor version', async () => { + const first = await resolvePackageManager('yarn@1.22'); + expect(first.executable).toBe('yarn'); + expect(first.version).toBe('1.22'); + }); + + it('should accept a major version', async () => { + const first = await resolvePackageManager('yarn@1'); + expect(first.executable).toBe('yarn'); + expect(first.version).toBe('1'); + }); + + it('should cache explicit argument and ignore later env / lockfile', async () => { + const first = await resolvePackageManager('pnpm@10.0.0'); + 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'); + }); + }); }); describe('spawnPackageManager', () => { @@ -175,36 +238,4 @@ 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 8ac5287c20..23b3b1b2f2 100644 --- a/packages/utils/core-utils/src/package-manager.ts +++ b/packages/utils/core-utils/src/package-manager.ts @@ -76,10 +76,11 @@ function pmFromUserAgent() { /** * Resolves the package manager to use. In order, it checks the following: * - * 1. The value of the `NODE_INSTALLER` environment variable. - * 2. The `process.env.npm_config_user_agent` value set by the executing package manager. - * 3. The presence of a lockfile in an ancestor directory. - * 4. If an unknown package manager is used (or none of the above apply), then we fall back to `npm`. + * 1. An explicit arg being passed into the function. + * 2. The value of the `NODE_INSTALLER` environment variable. + * 3. The `process.env.npm_config_user_agent` value set by the executing package manager. + * 4. The presence of a lockfile in an ancestor directory. + * 5. If an unknown package manager is used (or none of the above apply), then we fall back to `npm`. * * The version of the executing package manager is also returned if it is detected via user agent. * @@ -89,6 +90,31 @@ function pmFromUserAgent() { export const resolvePackageManager: ( packageManager?: string, ) => Promise = async (packageManager) => { + let installer: string | undefined; + let installerVersion: string | undefined; + + if (explicitPMCache) { + d(`Using cached explicit package manager: ${explicitPMCache.executable}`); + return explicitPMCache; + } + + if (packageManager) { + const match = packageManager.match( + /^(npm|pnpm|yarn)(?:@(latest|\d+(?:\.\d+)?(?:\.\d+)?(?:-.+)?))?$/, + ); + + if (match) { + const [, executable, version] = match; + if (Object.keys(PACKAGE_MANAGERS).includes(executable)) { + const pm = PACKAGE_MANAGERS[executable as SupportedPackageManager]; + installerVersion = version ?? 'latest'; + explicitPMCache = { ...pm, version: installerVersion }; + d(`Resolved and cached explicit package manager: ${pm.executable}`); + return explicitPMCache; + } + } + } + const executingPM = pmFromUserAgent(); let lockfilePM; const lockfile = await findUp( @@ -100,30 +126,7 @@ export const resolvePackageManager: ( lockfilePM = PM_FROM_LOCKFILE[lockfileName]; } - let installer: string | undefined; - let installerVersion: string | undefined; - - 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 (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 ecd41d03b047d4497a507a00c32e97b58c260149 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 18 Dec 2025 18:55:10 -0800 Subject: [PATCH 06/19] commit --- .../spec/slow/init.slow.verdaccio.spec.ts | 27 +-------- packages/api/core/src/api/init.ts | 2 +- .../core-utils/spec/electron-version.spec.ts | 14 +++-- .../core-utils/spec/package-manager.spec.ts | 57 +------------------ .../utils/core-utils/src/package-manager.ts | 38 ++----------- 5 files changed, 21 insertions(+), 117 deletions(-) diff --git a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts index 2bd85e9d10..9bbff06e51 100644 --- a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts +++ b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts @@ -214,18 +214,11 @@ describe('init', () => { describe('package managers', () => { describe('with npm', () => { beforeAll(() => { - const originalPM = process.env.NODE_INSTALLER; - process.env.NODE_INSTALLER = 'npm'; - process.env.COREPACK_ENABLE_STRICT = '0'; - - return () => { - process.env.NODE_INSTALLER = originalPM; - }; }); it('initializes with package-lock.json', async () => { - await api.init({ dir }); + await api.init({ dir, packageManager: 'npm' }); expect(fs.existsSync(path.join(dir, 'package-lock.json'))).toBe(true); expect(fs.existsSync(path.join(dir, 'yarn.lock'))).toBe(false); @@ -236,15 +229,8 @@ describe('init', () => { // NOTE: we basically run all tests via Yarn Berry anyways // due to the `packageManager` entry in this monorepo. describe('with yarn (berry)', () => { - beforeAll(() => { - const originalPM = process.env.NODE_INSTALLER; - process.env.NODE_INSTALLER = 'yarn'; - return () => { - process.env.NODE_INSTALLER = originalPM; - }; - }); it('initializes with correct nodeLinker value', async () => { - await api.init({ dir }); + await api.init({ dir, packageManager: 'yarn' }); expect( fs.readFileSync(path.join(dir, '.yarnrc.yml'), 'utf-8'), @@ -270,19 +256,12 @@ describe('init', () => { describe('with pnpm', () => { beforeAll(() => { - const originalPM = process.env.NODE_INSTALLER; - process.env.NODE_INSTALLER = 'pnpm'; - // disable corepack strict to allow pnpm to be used process.env.COREPACK_ENABLE_STRICT = '0'; - - return () => { - process.env.NODE_INSTALLER = originalPM; - }; }); it('initializes with correct node-linker value', async () => { - await api.init({ dir }); + await api.init({ dir, packageManager: 'pnpm' }); expect(fs.readFileSync(path.join(dir, '.npmrc'), 'utf-8')).toContain( 'node-linker = hoisted', diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index 245221ce30..74a1598a36 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -56,7 +56,7 @@ export interface InitOptions { */ electronVersion?: string; /** - * Force a package manager to use (npm|yarn|pnpm). Internally sets NODE_INSTALLER (deprecated upstream) to ensure template PM-specific logic runs. + * Force a package manager to use (npm|yarn|pnpm). */ packageManager?: string; } diff --git a/packages/utils/core-utils/spec/electron-version.spec.ts b/packages/utils/core-utils/spec/electron-version.spec.ts index c164814bc2..c724cc8932 100644 --- a/packages/utils/core-utils/spec/electron-version.spec.ts +++ b/packages/utils/core-utils/spec/electron-version.spec.ts @@ -117,8 +117,11 @@ describe('getElectronVersion', () => { }); describe('with yarn workspaces', () => { + let originalUserAgent: string | undefined; beforeAll(() => { - process.env.NODE_INSTALLER = 'yarn'; + originalUserAgent = process.env.npm_config_user_agent; + process.env.npm_config_user_agent = + 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; }); it('works with a non-exact version', async () => { @@ -139,7 +142,7 @@ describe('getElectronVersion', () => { }); afterAll(() => { - delete process.env.NODE_INSTALLER; + process.env.npm_config_user_agent = originalUserAgent; }); }); }); @@ -213,12 +216,15 @@ describe('getElectronModulePath', () => { }); describe('with yarn workspaces', () => { + let originalUserAgent: string | undefined; beforeAll(() => { - process.env.NODE_INSTALLER = 'yarn'; + originalUserAgent = process.env.npm_config_user_agent; + process.env.npm_config_user_agent = + 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; }); afterAll(() => { - delete process.env.NODE_INSTALLER; + process.env.npm_config_user_agent = originalUserAgent; }); it('finds the top-level electron module', async () => { diff --git a/packages/utils/core-utils/spec/package-manager.spec.ts b/packages/utils/core-utils/spec/package-manager.spec.ts index 3dc42f9929..67942a0bb9 100644 --- a/packages/utils/core-utils/spec/package-manager.spec.ts +++ b/packages/utils/core-utils/spec/package-manager.spec.ts @@ -87,59 +87,6 @@ describe('package-manager', () => { }); }); - describe('NODE_INSTALLER', () => { - let initialNodeInstallerValue: string | undefined; - - beforeEach(() => { - initialNodeInstallerValue = process.env.NODE_INSTALLER; - delete process.env.NODE_INSTALLER; - // NODE_INSTALLER is deprecated for Electron Forge 8 and throws a console.warn that we want to silence in tests - vi.spyOn(console, 'warn').mockImplementation(() => undefined); - - return () => { - // For cleanup, we want to restore process.env.NODE_INSTALLER. - // If it wasn't explicitly set before, we delete the value set during the test. - // Otherwise, we restore the initial value. - if (!initialNodeInstallerValue) { - delete process.env.NODE_INSTALLER; - } else { - process.env.NODE_INSTALLER = initialNodeInstallerValue; - } - vi.restoreAllMocks(); - }; - }); - - it.each([{ pm: 'yarn' }, { pm: 'npm' }, { pm: 'pnpm' }])( - 'should return $pm if NODE_INSTALLER=$pm', - async ({ pm }) => { - process.env.NODE_INSTALLER = pm; - vi.mocked(spawn).mockResolvedValue('9.9.9'); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - pm, - ); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'version', - '9.9.9', - ); - }, - ); - - it('should return npm if package manager is unsupported', async () => { - process.env.NODE_INSTALLER = 'bun'; - console.warn = vi.fn(); - vi.mocked(spawn).mockResolvedValue('1.22.22'); - await expect(resolvePackageManager()).resolves.toHaveProperty( - 'executable', - 'npm', - ); - expect(console.warn).toHaveBeenCalledWith( - '⚠', - expect.stringContaining('Package manager bun is unsupported'), - ); - }); - }); - it('should use the package manager for the nearest ancestor lockfile if detected', async () => { delete process.env.npm_config_user_agent; vi.mocked(findUp).mockResolvedValue('/Users/foo/bar/yarn.lock'); @@ -167,7 +114,6 @@ describe('package-manager', () => { describe('with an explicit package manager passed in', () => { beforeEach(() => { __resetExplicitPMCacheForTests(); - delete process.env.NODE_INSTALLER; delete process.env.npm_config_user_agent; }); @@ -206,7 +152,8 @@ describe('package-manager', () => { expect(first.executable).toBe('pnpm'); expect(first.version).toBe('10.0.0'); - process.env.NODE_INSTALLER = 'yarn'; + process.env.npm_config_user_agent = + 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64'; vi.mocked(spawn).mockResolvedValue('9.9.9'); const second = await resolvePackageManager(); expect(second.executable).toBe('pnpm'); diff --git a/packages/utils/core-utils/src/package-manager.ts b/packages/utils/core-utils/src/package-manager.ts index 23b3b1b2f2..8c67695da7 100644 --- a/packages/utils/core-utils/src/package-manager.ts +++ b/packages/utils/core-utils/src/package-manager.ts @@ -5,10 +5,8 @@ import { CrossSpawnOptions, spawn, } from '@malept/cross-spawn-promise'; -import chalk from 'chalk'; import debug from 'debug'; import findUp from 'find-up'; -import logSymbols from 'log-symbols'; const d = debug('electron-forge:package-manager'); @@ -21,7 +19,6 @@ export type PMDetails = { exact: string; }; -let hasWarned = false; let explicitPMCache: PMDetails | undefined; /** @@ -77,10 +74,9 @@ function pmFromUserAgent() { * Resolves the package manager to use. In order, it checks the following: * * 1. An explicit arg being passed into the function. - * 2. The value of the `NODE_INSTALLER` environment variable. - * 3. The `process.env.npm_config_user_agent` value set by the executing package manager. - * 4. The presence of a lockfile in an ancestor directory. - * 5. If an unknown package manager is used (or none of the above apply), then we fall back to `npm`. + * 2. The `process.env.npm_config_user_agent` value set by the executing package manager. + * 3. The presence of a lockfile in an ancestor directory. + * 4. If an unknown package manager is used (or none of the above apply), then we fall back to `npm`. * * The version of the executing package manager is also returned if it is detected via user agent. * @@ -126,31 +122,7 @@ export const resolvePackageManager: ( lockfilePM = PM_FROM_LOCKFILE[lockfileName]; } - 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( - PACKAGE_MANAGERS[installer as SupportedPackageManager], - ['--version'], - ); - if (!hasWarned) { - console.warn( - logSymbols.warning, - chalk.yellow( - `The NODE_INSTALLER environment variable is deprecated and will be removed in Electron Forge v8`, - ), - ); - hasWarned = true; - } - } else { - console.warn( - logSymbols.warning, - chalk.yellow( - `Package manager ${chalk.red(process.env.NODE_INSTALLER)} is unsupported. Falling back to ${chalk.green('npm')} instead.`, - ), - ); - } - } else if (executingPM) { + if (executingPM) { installer = executingPM.name; installerVersion = executingPM.version; } else if (lockfilePM) { @@ -166,7 +138,7 @@ export const resolvePackageManager: ( case 'npm': case 'pnpm': d( - `Resolved package manager to ${installer}. (Derived from NODE_INSTALLER: ${process.env.NODE_INSTALLER}, npm_config_user_agent: ${process.env.npm_config_user_agent}, lockfile: ${lockfilePM})`, + `Resolved package manager to ${installer}. (Derived from npm_config_user_agent: ${process.env.npm_config_user_agent}, lockfile: ${lockfilePM})`, ); return { ...PACKAGE_MANAGERS[installer], From aaaae22ddd46f5a55915b1f563d4a8d4c67139aa Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 18 Dec 2025 19:02:08 -0800 Subject: [PATCH 07/19] add missing verdaccio flag --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ffb94d691..781325fc77 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "lint": "npm run lint:js && npm run lint:markdown && npm run lint:markdown-js && npm run lint:markdown-links", "lint:fix": "prettier --write . --experimental-cli && eslint --fix . --cache", "prepack": "yarn build", - "test": "xvfb-maybe vitest run --project fast --project slow", + "test": "xvfb-maybe vitest run --project fast --project slow --project slow-verdaccio", "test:fast": "xvfb-maybe vitest run --project fast", "test:slow": "xvfb-maybe vitest run --project slow", "test:verdaccio": "tsx tools/verdaccio/spawn-verdaccio.ts xvfb-maybe vitest run --project slow-verdaccio", From dfebe336ed5d070f6e9d999dfe9087e9046cab2c Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 19 Dec 2025 12:03:00 -0800 Subject: [PATCH 08/19] modify verdaccio script --- package.json | 3 ++- tools/verdaccio/spawn-verdaccio.ts | 36 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 781325fc77..86a7270e6c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "test:verdaccio": "tsx tools/verdaccio/spawn-verdaccio.ts xvfb-maybe vitest run --project slow-verdaccio", "test:clear": "tsx tools/test-clear", "update:lockfile-fixtures": "tsx tools/regenerate-lockfile-fixtures.ts", - "postinstall": "husky install && node -e \"try { fs.rmSync('node_modules/.bin/*.ps1', { recursive: true, force: true }) } catch (e) {}\" && tsx ./tools/gen-tsconfigs.ts && tsx ./tools/gen-ts-glue.ts" + "postinstall": "husky install && node -e \"try { fs.rmSync('node_modules/.bin/*.ps1', { recursive: true, force: true }) } catch (e) {}\" && tsx ./tools/gen-tsconfigs.ts && tsx ./tools/gen-ts-glue.ts", + "spawn-verdaccio": "tsx tools/verdaccio/spawn-verdaccio.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.654.0", diff --git a/tools/verdaccio/spawn-verdaccio.ts b/tools/verdaccio/spawn-verdaccio.ts index 9612b0b276..4b8c9ec82e 100644 --- a/tools/verdaccio/spawn-verdaccio.ts +++ b/tools/verdaccio/spawn-verdaccio.ts @@ -8,10 +8,11 @@ * the latest and greatest. * * Usage: - * tsx tools/verdaccio/spawn-verdaccio.ts [args...] + * tsx tools/verdaccio/spawn-verdaccio.ts [command] [args...] * - * Example: - * tsx tools/verdaccio-spawn-verdaccio.ts yarn test:slow + * Examples: + * tsx tools/verdaccio/spawn-verdaccio.ts yarn test:slow + * tsx tools/verdaccio/spawn-verdaccio.ts # Keeps Verdaccio running for manual testing */ import { ChildProcess, spawn } from 'node:child_process'; @@ -154,32 +155,31 @@ async function runCommand(args: string[]) { async function main(): Promise { const args = process.argv.slice(2); - if (args.length === 0) { - console.error( - 'Usage: tsx tools/verdaccio/spawn-verdaccio.ts [args...]', - ); - console.error( - 'Example: tsx tools/verdaccio/spawn-verdaccio.ts yarn test:slow', - ); - process.exit(1); - } - // Handle signals process.on('SIGINT', () => { stopVerdaccio(); - process.exit(1); + process.exit(0); }); process.on('SIGTERM', () => { stopVerdaccio(); - process.exit(1); + process.exit(0); }); try { await startVerdaccio(); await publishPackages(); - await runCommand(args); - stopVerdaccio(); - process.exit(0); + + if (args.length === 0) { + // No command provided - keep Verdaccio running for manual testing + console.log(`\n✅ Verdaccio is running at ${VERDACCIO_URL}`); + console.log(' Press Ctrl+C to stop.\n'); + // Keep the process alive + await new Promise(() => {}); + } else { + await runCommand(args); + stopVerdaccio(); + process.exit(0); + } } catch (error) { console.error('❌ Error:', error); stopVerdaccio(); From b1ee6e38df3daa3a0ede6787d3bae508914bfaa4 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 19 Dec 2025 12:39:39 -0800 Subject: [PATCH 09/19] remove pnpm install from ci --- .github/workflows/ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e0dc69fd1..9b56415645 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,9 +131,6 @@ jobs: libgbm1 sudo add-apt-repository -y ppa:alexlarsson/flatpak - - name: Install pnpm - run: npm install -g pnpm@10.0.0 - - name: Run fast tests run: | mkdir -p ./reports/out @@ -213,9 +210,6 @@ jobs: libgtk-3-0 \ libgbm1 - - name: Install pnpm - run: npm install -g pnpm@10.0.0 - - name: Run slow tests run: | mkdir -p ./reports/out From 052e51836256f09480ee13794f02af45fe020759 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 19 Dec 2025 12:51:16 -0800 Subject: [PATCH 10/19] try to put a version number for yarn --- packages/api/core/spec/slow/init.slow.verdaccio.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts index 9bbff06e51..0c3e963283 100644 --- a/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts +++ b/packages/api/core/spec/slow/init.slow.verdaccio.spec.ts @@ -230,7 +230,7 @@ describe('init', () => { // due to the `packageManager` entry in this monorepo. describe('with yarn (berry)', () => { it('initializes with correct nodeLinker value', async () => { - await api.init({ dir, packageManager: 'yarn' }); + await api.init({ dir, packageManager: 'yarn@4.10.3' }); expect( fs.readFileSync(path.join(dir, '.yarnrc.yml'), 'utf-8'), From c24bf97f6bb1c2762e27ae694e49ce53bdd31b30 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 19 Dec 2025 13:01:53 -0800 Subject: [PATCH 11/19] fixup --- packages/utils/core-utils/src/package-manager.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/utils/core-utils/src/package-manager.ts b/packages/utils/core-utils/src/package-manager.ts index 8c67695da7..95b0c0ef08 100644 --- a/packages/utils/core-utils/src/package-manager.ts +++ b/packages/utils/core-utils/src/package-manager.ts @@ -89,11 +89,8 @@ export const resolvePackageManager: ( let installer: string | undefined; let installerVersion: string | undefined; - if (explicitPMCache) { - d(`Using cached explicit package manager: ${explicitPMCache.executable}`); - return explicitPMCache; - } - + // Check explicit packageManager argument FIRST, before cache + // This ensures explicit args always take precedence if (packageManager) { const match = packageManager.match( /^(npm|pnpm|yarn)(?:@(latest|\d+(?:\.\d+)?(?:\.\d+)?(?:-.+)?))?$/, @@ -111,6 +108,11 @@ export const resolvePackageManager: ( } } + if (explicitPMCache) { + d(`Using cached explicit package manager: ${explicitPMCache.executable}`); + return explicitPMCache; + } + const executingPM = pmFromUserAgent(); let lockfilePM; const lockfile = await findUp( From 92294ba4b7e0f76379ad9312f370972a815dffb8 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 19 Dec 2025 13:29:42 -0800 Subject: [PATCH 12/19] Revert "remove pnpm install from ci" This reverts commit b1ee6e38df3daa3a0ede6787d3bae508914bfaa4. --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b56415645..3e0dc69fd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,9 @@ jobs: libgbm1 sudo add-apt-repository -y ppa:alexlarsson/flatpak + - name: Install pnpm + run: npm install -g pnpm@10.0.0 + - name: Run fast tests run: | mkdir -p ./reports/out @@ -210,6 +213,9 @@ jobs: libgtk-3-0 \ libgbm1 + - name: Install pnpm + run: npm install -g pnpm@10.0.0 + - name: Run slow tests run: | mkdir -p ./reports/out From d62a4bc566bc6e6b1af0ea3487b73b08cf7dba7e Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 19 Dec 2025 13:32:14 -0800 Subject: [PATCH 13/19] attempt to prune --- tools/verdaccio/spawn-verdaccio.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/verdaccio/spawn-verdaccio.ts b/tools/verdaccio/spawn-verdaccio.ts index 4b8c9ec82e..07f59d2970 100644 --- a/tools/verdaccio/spawn-verdaccio.ts +++ b/tools/verdaccio/spawn-verdaccio.ts @@ -133,6 +133,9 @@ async function publishPackages(): Promise { } async function runCommand(args: string[]) { + console.log('🗑️ Pruning pnpm store before running command'); + await spawnPromise('pnpm', ['store', 'prune']); + console.log(`🏃 Running: ${args.join(' ')}`); console.log(` Using registry: ${VERDACCIO_URL}`); From 22303f76cc7fea414a4b54c092644c5aeeb69df3 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Fri, 19 Dec 2025 13:59:20 -0800 Subject: [PATCH 14/19] replace cache clearing fn with dynamic import in tests --- .../core-utils/spec/package-manager.spec.ts | 24 +++++++++++++++++-- .../utils/core-utils/src/package-manager.ts | 5 ---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/utils/core-utils/spec/package-manager.spec.ts b/packages/utils/core-utils/spec/package-manager.spec.ts index 67942a0bb9..34f0471b05 100644 --- a/packages/utils/core-utils/spec/package-manager.spec.ts +++ b/packages/utils/core-utils/spec/package-manager.spec.ts @@ -3,7 +3,6 @@ import findUp from 'find-up'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { - __resetExplicitPMCacheForTests, resolvePackageManager, spawnPackageManager, } from '../src/package-manager'; @@ -113,41 +112,59 @@ describe('package-manager', () => { describe('with an explicit package manager passed in', () => { beforeEach(() => { - __resetExplicitPMCacheForTests(); + vi.resetModules(); delete process.env.npm_config_user_agent; }); it('should accept a string with only the package manager anme', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); const first = await resolvePackageManager('pnpm'); expect(first.executable).toBe('pnpm'); expect(first.version).toBe('latest'); }); it('should accept the @latest tag', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); const first = await resolvePackageManager('yarn@latest'); expect(first.executable).toBe('yarn'); expect(first.version).toBe('latest'); }); it('should accept a full version', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); const first = await resolvePackageManager('yarn@1.22.22'); expect(first.executable).toBe('yarn'); expect(first.version).toBe('1.22.22'); }); it('should accept a major.minor version', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); const first = await resolvePackageManager('yarn@1.22'); expect(first.executable).toBe('yarn'); expect(first.version).toBe('1.22'); }); it('should accept a major version', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); const first = await resolvePackageManager('yarn@1'); expect(first.executable).toBe('yarn'); expect(first.version).toBe('1'); }); it('should cache explicit argument and ignore later env / lockfile', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); const first = await resolvePackageManager('pnpm@10.0.0'); expect(first.executable).toBe('pnpm'); expect(first.version).toBe('10.0.0'); @@ -161,6 +178,9 @@ describe('package-manager', () => { }); it('should fallback to npm and cache when explicit argument unsupported', async () => { + const { resolvePackageManager } = await import( + '../src/package-manager' + ); vi.mocked(spawn).mockResolvedValue('9.99.99'); const result = await resolvePackageManager('good coffee'); expect(result.executable).toBe('npm'); diff --git a/packages/utils/core-utils/src/package-manager.ts b/packages/utils/core-utils/src/package-manager.ts index 95b0c0ef08..2e7c6f0b7c 100644 --- a/packages/utils/core-utils/src/package-manager.ts +++ b/packages/utils/core-utils/src/package-manager.ts @@ -164,8 +164,3 @@ 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 57be48c53bf0ba6a3ef75e7792a6568aa544adf2 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 29 Dec 2025 11:58:20 -0800 Subject: [PATCH 15/19] chore: don't use corepack for npm --- packages/api/core/src/api/init.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index 74a1598a36..79a16e5c0a 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -177,6 +177,7 @@ export default async ({ }, { title: `Setting package manager with Corepack`, + enabled: ({ pm }) => pm.executable !== 'npm', task: async ({ pm }, task) => { const pmString = `${pm.executable}@${pm.version}`; try { From 891668a28c460446ca1336f277068e128520a563 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Mon, 29 Dec 2025 12:49:49 -0800 Subject: [PATCH 16/19] Revert "chore: don't use corepack for npm" This reverts commit 57be48c53bf0ba6a3ef75e7792a6568aa544adf2. --- packages/api/core/src/api/init.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index 79a16e5c0a..74a1598a36 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -177,7 +177,6 @@ export default async ({ }, { title: `Setting package manager with Corepack`, - enabled: ({ pm }) => pm.executable !== 'npm', task: async ({ pm }, task) => { const pmString = `${pm.executable}@${pm.version}`; try { From 0faa24ecbf1b76736cb192535c864cfa8d974124 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Tue, 6 Jan 2026 09:43:51 -0800 Subject: [PATCH 17/19] fix typo in test name --- packages/utils/core-utils/spec/package-manager.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/core-utils/spec/package-manager.spec.ts b/packages/utils/core-utils/spec/package-manager.spec.ts index 34f0471b05..9e21a1331b 100644 --- a/packages/utils/core-utils/spec/package-manager.spec.ts +++ b/packages/utils/core-utils/spec/package-manager.spec.ts @@ -116,7 +116,7 @@ describe('package-manager', () => { delete process.env.npm_config_user_agent; }); - it('should accept a string with only the package manager anme', async () => { + it('should accept a string with only the package manager name', async () => { const { resolvePackageManager } = await import( '../src/package-manager' ); From 4b84a7fd0c88a75468ebaa0ba6742643cbe4112a Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Wed, 7 Jan 2026 09:46:11 -0800 Subject: [PATCH 18/19] chore: only use corepack for non-npm --- packages/api/core/src/api/init.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index 74a1598a36..c2c726565b 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -177,6 +177,9 @@ export default async ({ }, { title: `Setting package manager with Corepack`, + // pm.executable needs to be optional here because the code gets evaluated twice (on init and on execution) + // @see https://listr2.kilic.dev/task/enable.html + enabled: ({ pm }) => pm?.executable !== 'npm', task: async ({ pm }, task) => { const pmString = `${pm.executable}@${pm.version}`; try { From 31c7e32b6be1ce45afbba168ee2eff2910525764 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Wed, 7 Jan 2026 16:52:00 -0800 Subject: [PATCH 19/19] add debug log --- packages/utils/core-utils/src/package-manager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/utils/core-utils/src/package-manager.ts b/packages/utils/core-utils/src/package-manager.ts index 2e7c6f0b7c..2a0a3422c4 100644 --- a/packages/utils/core-utils/src/package-manager.ts +++ b/packages/utils/core-utils/src/package-manager.ts @@ -104,6 +104,10 @@ export const resolvePackageManager: ( explicitPMCache = { ...pm, version: installerVersion }; d(`Resolved and cached explicit package manager: ${pm.executable}`); return explicitPMCache; + } else { + d( + `Attempted to parse ${packageManager} to regex but failed. Falling back!`, + ); } } }