Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): pnpm support #3822

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open

feat(core): pnpm support #3822

wants to merge 20 commits into from

Conversation

erickzhao
Copy link
Member

@erickzhao erickzhao commented Jan 28, 2025

Closes #2633
Closes #3574

ref #3813

This PR aims to provide a basic level of pnpm support for Electron Forge.

Context

Under the hood, Forge's init and import commands use a Node.js package manager to install dependencies and otherwise execute commands. The logic is set up to use yarn if detected, and falls back to npm otherwise.

With the proliferation of alternative package managers in recent years, one of the most popular requests on our issue tracker was to support pnpm (#2633).

However, this required a decent amount of refactoring since we relied on the unmaintained (yarn-or-npm) package to perform this package manager detection. This locked us out of support for other package managers until this utility was completely refactored.

Prior art

Big thanks to @makeryi and @goosewobbler for opening their respective PRs attempting support (#3351 and #3574).

Based on their efforts, I first landed #3813, which replaces yarn-or-npm with the more generalized detect-package-manager package without changing the API signature for our utility APIs.

This PR follows that up by replacing our package manager utilities (which still only supported yarn or npm) with a generic package manager detection system that returns the current package manager as well as its associated commands and flags.

Implementation

Package manager detection

The core of this PR refactors the yarn-or-npm file in @electron-forge/core-utils to instead be a generic package-manager util.

Instead of returning just the name of the package manager, resolving which package manager to use also returns its install command as well as the command flags we may want to use.

export type SupportedPackageManager = 'yarn' | 'npm' | 'pnpm';
export type PMDetails = { executable: SupportedPackageManager; install: string; dev: string; exact: string };
const MANAGERS: Record<SupportedPackageManager, PMDetails> = {
yarn: {
executable: 'yarn',
install: 'add',
dev: '--dev',
exact: '--exact',
},
npm: {
executable: 'npm',
install: 'install',
dev: '--save-dev',
exact: '--save-exact',
},
pnpm: {
executable: 'pnpm',
install: 'add',
dev: '--save-dev',
exact: '--save-exact',
},
};

Note

In theory, we could do away with these objects for now because the shorthand <pm> install -E -D works across npm, yarn, and pnpm. However, this setup future-proofs us against future commands/flags that we'd want to add that aren't necessarily compatible.

The behaviour for resolvePackageManager now differs. If an unsupported package manager is detected via NODE_INSTALLER or detect-package-manager, we default to npm instead of whatever the detected system package manager is.

This solves the case where we'd detect an unsupported package manager (e.g. bun).

node-linker=hoisted

Out of the box, pnpm provides improved disk space efficiency and install speed because it symlinks all dependencies and stores them in a central location on disk (see Motivation section of the pnpm docs for more details).

However, Forge expects node_modules to exist on disk at specific locations because we bundle all production npm dependencies into the Electron app when packaging.

To that end, we expect the config to be node-linker=hoisted when running Electron Forge. I added a clause to check-system.ts to ensure this by checking the value of pnpm config get node-linker.

async function checkPnpmNodeLinker() {
const nodeLinkerValue = await spawnPackageManager(['config', 'get', 'node-linker']);
if (nodeLinkerValue !== 'hoisted') {
throw new Error('When using pnpm, `node-linker` must be set to "hoisted". Run `pnpm config set node-linker hoisted` to set this config value.');
}
}

This setting is added out of the box when initializing Forge templates with pnpm via .npmrc:

node-linker = hoisted

if (pm.executable === 'pnpm') {
rootFiles.push('.npmrc');
}

pnpm workspaces

I think we actually already supported pnpm workspaces via:

async function findAncestorNodeModulesPath(dir: string, packageName: string): Promise<string | undefined> {
d('Looking for a lock file to indicate the root of the repo');
const lockPath = await findUp(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'], { cwd: dir, type: 'file' });
if (lockPath) {
d(`Found lock file: ${lockPath}`);
const nodeModulesPath = path.join(path.dirname(lockPath), 'node_modules', packageName);
if (await fs.pathExists(nodeModulesPath)) {
return nodeModulesPath;
}
}
return Promise.resolve(undefined);
}

Supported pnpm version ranges

I'm not sure if v8.0.0 is a correct lower bound for pnpm, but it's the lowest version they have for their documentation so I'm assuming something has to do with their support policy.

Testing

This PR expands the existing test suite to also support pnpm (mostly in api.slow.spec.ts).

Note

Previously, we actually didn't test packaging/making for npm. This PR has the side effect of fully testing the API across all supported package managers, which does bring up the Time to Green for our CI.

@erickzhao erickzhao changed the title Pnpmpnpmpnpmpnpm feat: pnpm support Jan 28, 2025
@erickzhao erickzhao changed the title feat: pnpm support feat(core): pnpm support Jan 28, 2025
@@ -46,7 +46,7 @@ commands:
- run:
name: 'Run fast tests'
command: |
yarn test:fast --reporter=junit --outputFile="./reports/out/test_output.xml"
yarn test:fast --reporter=default --reporter=junit --outputFile="./reports/out/test_output.xml"
Copy link
Member Author

Choose a reason for hiding this comment

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

Using the default reporter both gives us a clearer idea of the failures in CI and prevents us from hitting the 10-minute CircleCI no output timeout.

- run:
name: 'Install pnpm'
command: |
npm install -g [email protected]
Copy link
Member Author

Choose a reason for hiding this comment

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

Could also do this in corepack in theory, but I found this to be easier to grok.

all: '>= 1.0.0',
},
pnpm: {
all: '>= 8.0.0',
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know if this is necessary, but the Node compatibility matrix in the pnpm docs only lists up to pnpm 8 so I feel like this is a decent lower bound: https://pnpm.io/installation#compatibility

Comment on lines -49 to -65

function warnIfPackageManagerIsntAKnownGoodVersion(packageManager: string, version: string, allowlistedVersions: { [key: string]: string }) {
const osVersions = allowlistedVersions[process.platform];
const versions = osVersions ? `${allowlistedVersions.all} || ${osVersions}` : allowlistedVersions.all;
const versionString = version.toString();
checkValidPackageManagerVersion(packageManager, versionString, versions);
}

async function checkPackageManagerVersion() {
const version = await forgeUtils.yarnOrNpmSpawn(['--version']);
const versionString = version.toString().trim();
if (await forgeUtils.hasYarn()) {
warnIfPackageManagerIsntAKnownGoodVersion('Yarn', versionString, YARN_ALLOWLISTED_VERSIONS);
return `yarn@${versionString}`;
} else {
warnIfPackageManagerIsntAKnownGoodVersion('NPM', versionString, NPM_ALLOWLISTED_VERSIONS);
return `npm@${versionString}`;
Copy link
Member Author

Choose a reason for hiding this comment

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

These functions were refactored out into a single checkPackageManager utility.

hasYarn = hasYarn;

yarnOrNpmSpawn = yarnOrNpmSpawn;
spawnPackageManager = spawnPackageManager;
Copy link
Member Author

Choose a reason for hiding this comment

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

Is a change in API signatures for @electron-forge/core-utils a breaking change? If it is, I can add a temporary shim for these utils and deprecate them.

@erickzhao erickzhao marked this pull request as ready for review January 30, 2025 00:44
@erickzhao erickzhao requested a review from a team as a code owner January 30, 2025 00:44
@erickzhao erickzhao requested a review from BlackHole1 January 30, 2025 00:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support pnpm
1 participant