-
-
Notifications
You must be signed in to change notification settings - Fork 541
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
feat(core): pnpm support #3822
Changes from all commits
7c77355
2f6ef3d
c70224d
0acb1f3
284e219
1d85d64
431116d
cafacc3
a19a6ea
15db452
89a0fd1
d1547a8
e23518f
dacb4e5
93c0ef8
3dc34f3
dccf638
8b0e22f
1592d85
d5a43a2
64ac631
d695b26
7a5ab70
3350dfe
65458b3
1b084df
336662c
33454a5
514f2a1
f3ff623
519aa9f
8fb93be
12168ac
f19e1cb
67ff824
2e96421
7825022
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
run-slow-tests: | ||
steps: | ||
|
@@ -57,7 +57,7 @@ commands: | |
- run: | ||
name: 'Run slow tests' | ||
command: | | ||
yarn test:slow --reporter=junit --outputFile="./reports/out/test_output.xml" | ||
yarn test:slow --reporter=default --reporter=junit --outputFile="./reports/out/test_output.xml" | ||
jobs: | ||
lint-and-build: | ||
|
@@ -110,6 +110,10 @@ jobs: | |
libgtk-3-0 \ | ||
libgbm1 | ||
sudo add-apt-repository -y ppa:alexlarsson/flatpak | ||
- run: | ||
name: 'Install pnpm' | ||
command: | | ||
npm install -g [email protected] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could also do this in |
||
- run-fast-tests | ||
- store_test_results: | ||
path: ./reports/ | ||
|
@@ -155,6 +159,10 @@ jobs: | |
libgdk-pixbuf2.0-dev \ | ||
libgtk-3-0 \ | ||
libgbm1 | ||
- run: | ||
name: 'Install pnpm' | ||
command: | | ||
npm install -g [email protected] | ||
- run-slow-tests | ||
- run: | ||
when: always # the report is generated on pass or fail | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Required to run pnpm in tests because `packageManager` is set to `yarn` in `package.json` | ||
package-manager-strict=false |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,121 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
import { resolvePackageManager, spawnPackageManager } from '@electron-forge/core-utils'; | ||
import { describe, expect, it, vi } from 'vitest'; | ||
|
||
import { checkValidPackageManagerVersion } from '../src/util/check-system'; | ||
import { checkPackageManager } from '../src/util/check-system'; | ||
|
||
describe('check-system', () => { | ||
describe('validPackageManagerVersion', () => { | ||
it('should consider whitelisted versions to be valid', () => { | ||
expect(() => checkValidPackageManagerVersion('NPM', '3.10.1', '^3.0.0')).not.toThrow(); | ||
vi.mock(import('@electron-forge/core-utils'), async (importOriginal) => { | ||
const mod = await importOriginal(); | ||
return { | ||
...mod, | ||
resolvePackageManager: vi.fn(), | ||
spawnPackageManager: vi.fn(), | ||
}; | ||
}); | ||
|
||
describe('checkPackageManager', () => { | ||
it('should consider allowlisted versions to be valid', async () => { | ||
vi.mocked(resolvePackageManager).mockResolvedValue({ | ||
executable: 'npm', | ||
install: 'install', | ||
dev: '--save-dev', | ||
exact: '--save-exact', | ||
}); | ||
vi.mocked(spawnPackageManager).mockResolvedValue('10.9.2'); | ||
await expect(checkPackageManager()).resolves.not.toThrow(); | ||
}); | ||
|
||
it('rejects versions that are outside of the supported range', async () => { | ||
vi.mocked(resolvePackageManager).mockResolvedValue({ | ||
executable: 'yarn', | ||
install: 'add', | ||
dev: '--dev', | ||
exact: '--exact', | ||
}); | ||
|
||
it('should consider Yarn nightly versions to be invalid', () => { | ||
expect(() => checkValidPackageManagerVersion('Yarn', '0.23.0-20170311.0515', '0.23.0')).toThrow(); | ||
// yarn 0.x unsupported | ||
vi.mocked(spawnPackageManager).mockResolvedValue('0.22.0'); | ||
await expect(checkPackageManager()).rejects.toThrow(); | ||
}); | ||
|
||
it('should consider Yarn nightly versions to be invalid', async () => { | ||
vi.mocked(resolvePackageManager).mockResolvedValue({ | ||
executable: 'yarn', | ||
install: 'add', | ||
dev: '--dev', | ||
exact: '--exact', | ||
}); | ||
vi.mocked(spawnPackageManager).mockResolvedValue('0.23.0-20170311.0515'); | ||
await expect(checkPackageManager()).rejects.toThrow(); | ||
}); | ||
|
||
it('should consider invalid semver versions to be invalid', async () => { | ||
vi.mocked(resolvePackageManager).mockResolvedValue({ | ||
executable: 'yarn', | ||
install: 'add', | ||
dev: '--dev', | ||
exact: '--exact', | ||
}); | ||
vi.mocked(spawnPackageManager).mockResolvedValue('1.22'); | ||
await expect(checkPackageManager()).rejects.toThrow(); | ||
}); | ||
|
||
it('should throw if using pnpm without node-linker=hoisted or custom hoist-pattern', async () => { | ||
vi.mocked(resolvePackageManager).mockResolvedValue({ | ||
executable: 'pnpm', | ||
install: 'add', | ||
dev: '--dev', | ||
exact: '--exact', | ||
}); | ||
vi.mocked(spawnPackageManager).mockImplementation((args) => { | ||
if (args?.join(' ') === 'config get node-linker') { | ||
return Promise.resolve('isolated'); | ||
} else if (args?.join(' ') === 'config get hoist-pattern') { | ||
return Promise.resolve('undefined'); | ||
} else if (args?.join(' ') === 'config get public-hoist-pattern') { | ||
return Promise.resolve('undefined'); | ||
} else if (args?.join(' ') === '--version') { | ||
return Promise.resolve('10.0.0'); | ||
} else { | ||
throw new Error('Unexpected command'); | ||
} | ||
}); | ||
await expect(checkPackageManager()).rejects.toThrow( | ||
'When using pnpm, `node-linker` must be set to "hoisted" (or a custom `hoist-pattern` or `public-hoist-pattern` must be defined). Run `pnpm config set node-linker hoisted` to set this config value, or add it to your project\'s `.npmrc` file.' | ||
); | ||
}); | ||
|
||
it.each(['hoist-pattern', 'public-hoist-pattern'])('should pass without validation if user has set %s in their pnpm config', async (cfg) => { | ||
vi.mocked(resolvePackageManager).mockResolvedValue({ | ||
executable: 'pnpm', | ||
install: 'add', | ||
dev: '--dev', | ||
exact: '--exact', | ||
}); | ||
vi.mocked(spawnPackageManager).mockImplementation((args) => { | ||
if (args?.join(' ') === 'config get node-linker') { | ||
return Promise.resolve('isolated'); | ||
} else if (args?.join(' ') === `config get ${cfg}`) { | ||
return Promise.resolve('["*eslint*","*babel*"]'); | ||
} else if (args?.join(' ') === '--version') { | ||
return Promise.resolve('10.0.0'); | ||
} else { | ||
return Promise.resolve('undefined'); | ||
} | ||
}); | ||
await expect(checkPackageManager()).resolves.not.toThrow(); | ||
}); | ||
|
||
it('should consider invalid semver versions to be invalid', () => { | ||
expect(() => checkValidPackageManagerVersion('Yarn', '0.22', '0.22.0')).toThrow(); | ||
// resolvePackageManager optionally returns a `version` if `npm_config_user_agent` was used to | ||
// resolve the package manager being used. | ||
it('should not shell out to child process if version was already parsed via npm_config_user_agent', async () => { | ||
vi.mocked(resolvePackageManager).mockResolvedValue({ | ||
executable: 'npm', | ||
install: 'install', | ||
dev: '--save-dev', | ||
exact: '--save-exact', | ||
version: '10.9.2', | ||
}); | ||
await expect(checkPackageManager()).resolves.not.toThrow(); | ||
expect(spawnPackageManager).not.toHaveBeenCalled(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ import { exec } from 'node:child_process'; | |
import os from 'node:os'; | ||
import path from 'node:path'; | ||
|
||
import { utils as forgeUtils } from '@electron-forge/core'; | ||
import { resolvePackageManager, spawnPackageManager, SupportedPackageManager } from '@electron-forge/core-utils'; | ||
import { ForgeListrTask } from '@electron-forge/shared-types'; | ||
import debug from 'debug'; | ||
import fs from 'fs-extra'; | ||
|
@@ -27,43 +27,70 @@ async function checkNodeVersion() { | |
return process.versions.node; | ||
} | ||
|
||
const NPM_ALLOWLISTED_VERSIONS = { | ||
all: '^3.0.0 || ^4.0.0 || ~5.1.0 || ~5.2.0 || >= 5.4.2', | ||
darwin: '>= 5.4.0', | ||
linux: '>= 5.4.0', | ||
}; | ||
const YARN_ALLOWLISTED_VERSIONS = { | ||
all: '>= 1.0.0', | ||
}; | ||
/** | ||
* Packaging an app with Electron Forge requires `node_modules` to be on disk. | ||
* With `pnpm`, this can be done in a few different ways. | ||
* | ||
* `node-linker=hoisted` replicates the behaviour of npm and Yarn Classic, while | ||
* users may choose to set `public-hoist-pattern` or `hoist-pattern` for advanced | ||
* configuration purposes. | ||
*/ | ||
async function checkPnpmConfig() { | ||
const hoistPattern = await spawnPackageManager(['config', 'get', 'hoist-pattern']); | ||
const publicHoistPattern = await spawnPackageManager(['config', 'get', 'public-hoist-pattern']); | ||
|
||
export function checkValidPackageManagerVersion(packageManager: string, version: string, allowlistedVersions: string) { | ||
if (!semver.valid(version)) { | ||
d(`Invalid semver-string while checking version: ${version}`); | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
throw new Error(`Could not check ${packageManager} version "${version}", assuming incompatible`); | ||
if (hoistPattern !== 'undefined' || publicHoistPattern !== 'undefined') { | ||
d( | ||
`Custom hoist pattern detected ${JSON.stringify({ | ||
hoistPattern, | ||
publicHoistPattern, | ||
})}, assuming that the user has configured pnpm to package dependencies.` | ||
); | ||
return; | ||
} | ||
if (!semver.satisfies(version, allowlistedVersions)) { | ||
throw new Error(`Incompatible version of ${packageManager} detected "${version}", must be in range ${allowlistedVersions}`); | ||
|
||
const nodeLinker = await spawnPackageManager(['config', 'get', 'node-linker']); | ||
if (nodeLinker !== 'hoisted') { | ||
throw new Error( | ||
'When using pnpm, `node-linker` must be set to "hoisted" (or a custom `hoist-pattern` or `public-hoist-pattern` must be defined). Run `pnpm config set node-linker hoisted` to set this config value, or add it to your project\'s `.npmrc` file.' | ||
); | ||
} | ||
} | ||
|
||
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); | ||
} | ||
// TODO(erickzhao): Drop antiquated versions of npm for Forge v8 | ||
const ALLOWLISTED_VERSIONS: Record<SupportedPackageManager, Record<string, string>> = { | ||
npm: { | ||
all: '^3.0.0 || ^4.0.0 || ~5.1.0 || ~5.2.0 || >= 5.4.2', | ||
darwin: '>= 5.4.0', | ||
linux: '>= 5.4.0', | ||
}, | ||
yarn: { | ||
all: '>= 1.0.0', | ||
}, | ||
pnpm: { | ||
all: '>= 8.0.0', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
}, | ||
}; | ||
|
||
async function checkPackageManagerVersion() { | ||
const version = await forgeUtils.yarnOrNpmSpawn(['--version']); | ||
export async function checkPackageManager() { | ||
const pm = await resolvePackageManager(); | ||
const version = pm.version ?? (await spawnPackageManager(['--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}`; | ||
|
||
const range = ALLOWLISTED_VERSIONS[pm.executable][process.platform] ?? ALLOWLISTED_VERSIONS[pm.executable].all; | ||
if (!semver.valid(version)) { | ||
d(`Invalid semver-string while checking version: ${version}`); | ||
throw new Error(`Could not check ${pm.executable} version "${version}", assuming incompatible`); | ||
} | ||
if (!semver.satisfies(version, range)) { | ||
throw new Error(`Incompatible version of ${pm.executable} detected: "${version}" must be in range ${range}`); | ||
} | ||
|
||
if (pm.executable === 'pnpm') { | ||
await checkPnpmConfig(); | ||
} | ||
|
||
return `${pm.executable}@${versionString}`; | ||
} | ||
|
||
/** | ||
|
@@ -106,9 +133,9 @@ export async function checkSystem(task: ForgeListrTask<never>) { | |
}, | ||
}, | ||
{ | ||
title: 'Checking packageManager version', | ||
title: 'Checking package manager version', | ||
task: async (_, task) => { | ||
const packageManager = await checkPackageManagerVersion(); | ||
const packageManager = await checkPackageManager(); | ||
task.title = `Found ${packageManager}`; | ||
}, | ||
}, | ||
|
There was a problem hiding this comment.
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 CircleCIno output
timeout.