Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2a3b750
feat(init): add explicit package manager selection (npm/yarn/pnpm) to…
youaresoyoung Sep 12, 2025
8cb1323
feat(core-utils): cache explicit package manager selection and skip e…
youaresoyoung Sep 12, 2025
9945598
test(core-utils): add reset helper and specs for explicit package man…
youaresoyoung Sep 12, 2025
556a5da
feat(cli): unify package manager prompt via helper across init paths
youaresoyoung Sep 12, 2025
09fdf0a
feat(init): add `--package-manager` flag
erickzhao Dec 19, 2025
ecd41d0
commit
erickzhao Dec 19, 2025
aaaae22
add missing verdaccio flag
erickzhao Dec 19, 2025
dfebe33
modify verdaccio script
erickzhao Dec 19, 2025
b1ee6e3
remove pnpm install from ci
erickzhao Dec 19, 2025
052e518
try to put a version number for yarn
erickzhao Dec 19, 2025
c24bf97
fixup
erickzhao Dec 19, 2025
92294ba
Revert "remove pnpm install from ci"
erickzhao Dec 19, 2025
d62a4bc
attempt to prune
erickzhao Dec 19, 2025
22303f7
replace cache clearing fn with dynamic import in tests
erickzhao Dec 19, 2025
57be48c
chore: don't use corepack for npm
erickzhao Dec 29, 2025
891668a
Revert "chore: don't use corepack for npm"
erickzhao Dec 29, 2025
0faa24e
fix typo in test name
erickzhao Jan 6, 2026
4b84a7f
chore: only use corepack for non-npm
erickzhao Jan 7, 2026
c082951
Merge branch 'next' into node_installer_explicit
erickzhao Jan 7, 2026
31c7e32
add debug log
erickzhao Jan 8, 2026
2273a37
Merge branch 'node_installer_explicit' of github.com:electron/forge i…
erickzhao Jan 8, 2026
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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
"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",
"test:clear": "tsx tools/test-clear",
"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",
Expand Down
19 changes: 18 additions & 1 deletion packages/api/cli/src/electron-forge-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<InitOptions>(
Expand All @@ -46,11 +50,11 @@ 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<void> => {
// only run interactive prompts if no args passed and not in CI environment
if (
Object.keys(options).length > 0 ||
process.env.CI ||
Expand Down Expand Up @@ -79,6 +83,18 @@ program
}
}

const packageManager: string = await prompt.run<
Prompt<string, any>
>(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<Prompt<string, any>>(
select,
{
Expand Down Expand Up @@ -121,6 +137,7 @@ program
);
}

initOpts.packageManager = packageManager;
initOpts.template = `${bundler}${language ? `-${language}` : ''}`;

// TODO: add prompt for passing in an exact version as well
Expand Down
27 changes: 3 additions & 24 deletions packages/api/core/spec/slow/init.slow.verdaccio.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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@4.10.3' });

expect(
fs.readFileSync(path.join(dir, '.yarnrc.yml'), 'utf-8'),
Expand All @@ -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',
Expand Down
28 changes: 26 additions & 2 deletions packages/api/core/src/api/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -54,6 +55,10 @@ export interface InitOptions {
* @defaultValue The `latest` tag on npm.
*/
electronVersion?: string;
/**
* Force a package manager to use (npm|yarn|pnpm).
*/
packageManager?: string;
}

async function validateTemplate(
Expand Down Expand Up @@ -84,6 +89,7 @@ export default async ({
template = 'base',
skipGit = false,
electronVersion = 'latest',
packageManager,
}: InitOptions): Promise<void> => {
d(`Initializing in: ${dir}`);

Expand All @@ -96,8 +102,8 @@ export default async ({
{
title: `Resolving package manager`,
task: async (ctx, task) => {
ctx.pm = await resolvePackageManager();
task.title = `Resolving package manager: ${chalk.cyan(ctx.pm.executable)} v${ctx.pm.version}`;
ctx.pm = await resolvePackageManager(packageManager);
task.title = `Resolved package manager: ${chalk.cyan(`${ctx.pm.executable}@${ctx.pm.version}`)}`;
},
},
{
Expand Down Expand Up @@ -169,6 +175,24 @@ 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 {
await spawn('corepack', ['use', pmString], {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that corepack command is executed here regardless of whether the user passes in --package-manager or not. Would it be better to add a check here? When the user does not pass in --package-manager, the original behavior should be restored.

Also, if the --package-manager value is "npm", it can be skipped directly to avoid an unnecessary spawn call. This can also prevent the following description information from appearing when the user is using npm but has not installed corepack.

Overall, I support the changes in the current PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestions @BlackHole1! I'm going to incorporate those into the PR this week.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4b84a7f

I disabled the corepack step for npm, but after thinking about it a bit more, I think we should keep it even if --package-manager isn't passed in since you could get inferred package managers (e.g. via pnpm init)

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) => {
Expand Down
9 changes: 1 addition & 8 deletions packages/template/base/src/BaseTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down Expand Up @@ -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"';
Expand Down
14 changes: 10 additions & 4 deletions packages/utils/core-utils/spec/electron-version.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -139,7 +142,7 @@ describe('getElectronVersion', () => {
});

afterAll(() => {
delete process.env.NODE_INSTALLER;
process.env.npm_config_user_agent = originalUserAgent;
});
});
});
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading