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
21 changes: 20 additions & 1 deletion packages/api/cli/src/electron-forge-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,28 @@ program
},
{
task: async (initOpts, task): Promise<void> => {
// 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<Prompt<string, any>>(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;
}

Expand All @@ -73,6 +89,8 @@ program
}
}

const packageManager: string = await getPackageManager();

const bundler: string = await prompt.run<Prompt<string, any>>(
select,
{
Expand Down Expand Up @@ -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?`,
Expand Down
7 changes: 6 additions & 1 deletion packages/api/core/src/api/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -75,6 +79,7 @@ export default async ({
force = false,
template = 'base',
skipGit = false,
packageManager,
}: InitOptions): Promise<void> => {
d(`Initializing in: ${dir}`);

Expand All @@ -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)}`;
},
},
Expand Down
33 changes: 33 additions & 0 deletions packages/utils/core-utils/spec/package-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
});
});
});
36 changes: 32 additions & 4 deletions packages/utils/core-utils/src/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -85,7 +86,9 @@ function pmFromUserAgent() {
* Supported package managers are `yarn`, `pnpm`, and `npm`.
*
*/
export const resolvePackageManager: () => Promise<PMDetails> = async () => {
export const resolvePackageManager: (
packageManager?: string,
) => Promise<PMDetails> = async (packageManager) => {
const executingPM = pmFromUserAgent();
let lockfilePM;
const lockfile = await findUp(
Expand All @@ -97,10 +100,30 @@ export const resolvePackageManager: () => Promise<PMDetails> = 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(
Expand Down Expand Up @@ -164,3 +187,8 @@ export const spawnPackageManager = async (
): Promise<string> => {
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;
}