diff --git a/.circleci/config.yml b/.circleci/config.yml index a8566093f9..1a09d75244 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,6 +9,11 @@ commands: - run: git config --global core.autocrlf input - node/install: node-version: '18.15.0' + - run: + name: Install pnpm package manager + command: | + corepack enable + corepack prepare pnpm@latest-8 --activate - checkout run-lint-and-build: steps: diff --git a/package.json b/package.json index 4f353de2b0..048d2a51fa 100644 --- a/package.json +++ b/package.json @@ -80,8 +80,7 @@ "which": "^2.0.2", "xterm": "^4.9.0", "xterm-addon-fit": "^0.5.0", - "xterm-addon-search": "^0.8.0", - "yarn-or-npm": "^3.0.1" + "xterm-addon-search": "^0.8.0" }, "devDependencies": { "@electron/fuses": ">=1.0.0", diff --git a/packages/api/cli/src/util/check-system.ts b/packages/api/cli/src/util/check-system.ts index c64f8c4b70..5cd396eac5 100644 --- a/packages/api/cli/src/util/check-system.ts +++ b/packages/api/cli/src/util/check-system.ts @@ -57,14 +57,20 @@ function warnIfPackageManagerIsntAKnownGoodVersion(packageManager: string, versi } async function checkPackageManagerVersion() { - const version = await forgeUtils.yarnOrNpmSpawn(['--version']); + const version = await forgeUtils.packageManagerSpawn(['--version']); const versionString = version.toString().trim(); - if (forgeUtils.hasYarn()) { + const _isNpm = await forgeUtils.isNpm(); + const _isYarn = await forgeUtils.isYarn(); + if (_isYarn) { warnIfPackageManagerIsntAKnownGoodVersion('Yarn', versionString, YARN_ALLOWLISTED_VERSIONS); return `yarn@${versionString}`; - } else { + } else if (_isNpm) { warnIfPackageManagerIsntAKnownGoodVersion('NPM', versionString, NPM_ALLOWLISTED_VERSIONS); return `npm@${versionString}`; + } else { + // I think we don't need to check version of pnpm since 2023 + const pm = await forgeUtils.getPackageManager(); + return `${pm}@${versionString}`; } } diff --git a/packages/api/core/package.json b/packages/api/core/package.json index 4cbd757f38..69d8c791cd 100644 --- a/packages/api/core/package.json +++ b/packages/api/core/package.json @@ -72,8 +72,7 @@ "semver": "^7.2.1", "source-map-support": "^0.5.13", "sudo-prompt": "^9.1.1", - "username": "^5.1.0", - "yarn-or-npm": "^3.0.1" + "username": "^5.1.0" }, "engines": { "node": ">= 14.17.5" diff --git a/packages/api/core/src/api/import.ts b/packages/api/core/src/api/import.ts index 70bce074eb..8540ad0433 100644 --- a/packages/api/core/src/api/import.ts +++ b/packages/api/core/src/api/import.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { safeYarnOrNpm, updateElectronDependency } from '@electron-forge/core-utils'; +import { getPackageManager, updateElectronDependency } from '@electron-forge/core-utils'; import baseTemplate from '@electron-forge/template-base'; import chalk from 'chalk'; import debug from 'debug'; @@ -194,7 +194,7 @@ export default async ({ { title: 'Installing dependencies', task: async (_, task) => { - const packageManager = safeYarnOrNpm(); + const packageManager = getPackageManager(); await writeChanges(); d('deleting old dependencies forcefully'); @@ -202,15 +202,15 @@ export default async ({ await fs.remove(path.resolve(dir, 'node_modules/.bin/electron.cmd')); d('installing dependencies'); - task.output = `${packageManager} install ${importDeps.join(' ')}`; + task.output = `${packageManager} add ${importDeps.join(' ')}`; await installDepList(dir, importDeps); d('installing devDependencies'); - task.output = `${packageManager} install --dev ${importDevDeps.join(' ')}`; + task.output = `${packageManager} add -D ${importDevDeps.join(' ')}`; await installDepList(dir, importDevDeps, DepType.DEV); d('installing exactDevDependencies'); - task.output = `${packageManager} install --dev --exact ${importExactDevDeps.join(' ')}`; + task.output = `${packageManager} add -D -E ${importExactDevDeps.join(' ')}`; await installDepList(dir, importExactDevDeps, DepType.DEV, DepVersionRestriction.EXACT); }, }, @@ -258,7 +258,7 @@ export default async ({ }, task: (_, task) => { task.output = `We have attempted to convert your app to be in a format that Electron Forge understands. - + Thanks for using ${chalk.green('Electron Forge')}!`; }, }, diff --git a/packages/api/core/src/api/init-scripts/init-link.ts b/packages/api/core/src/api/init-scripts/init-link.ts index cd42418a27..cfb00af87c 100644 --- a/packages/api/core/src/api/init-scripts/init-link.ts +++ b/packages/api/core/src/api/init-scripts/init-link.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { safeYarnOrNpm, yarnOrNpmSpawn } from '@electron-forge/core-utils'; +import { getPackageManager, isPnpm, packageManagerSpawn } from '@electron-forge/core-utils'; import { ForgeListrTask } from '@electron-forge/shared-types'; import debug from 'debug'; @@ -22,14 +22,24 @@ export async function initLink(dir: string, task?: ForgeListrTask) { if (shouldLink) { d('Linking forge dependencies'); const packageJson = await readRawPackageJson(dir); - const packageManager = safeYarnOrNpm(); + const packageManager = getPackageManager(); const linkFolder = path.resolve(__dirname, '..', '..', '..', '..', '..', '..', '.links'); for (const packageName of Object.keys(packageJson.devDependencies)) { if (packageName.startsWith('@electron-forge/')) { - if (task) task.output = `${packageManager} link --link-folder ${linkFolder} ${packageName}`; - await yarnOrNpmSpawn(['link', '--link-folder', linkFolder, packageName], { - cwd: dir, - }); + if (task) { + const _isPnpm = await isPnpm(); + if (_isPnpm) { + task.output = `${packageManager} link ${linkFolder}/${packageName}`; + await packageManagerSpawn(['link', `${linkFolder}/${packageName}`], { + cwd: dir, + }); + } else { + task.output = `${packageManager} link --link-folder ${linkFolder} ${packageName}`; + await packageManagerSpawn(['link', '--link-folder', linkFolder, packageName], { + cwd: dir, + }); + } + } } } } else { diff --git a/packages/api/core/src/api/init-scripts/init-npm.ts b/packages/api/core/src/api/init-scripts/init-npm.ts index 9644a46102..e9ca77c819 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { safeYarnOrNpm } from '@electron-forge/core-utils'; +import { getPackageManager } from '@electron-forge/core-utils'; import { ForgeListrTask } from '@electron-forge/shared-types'; import debug from 'debug'; import fs from 'fs-extra'; @@ -27,17 +27,17 @@ export const exactDevDeps = ['electron']; export const initNPM = async (dir: string, task: ForgeListrTask): Promise => { d('installing dependencies'); - const packageManager = safeYarnOrNpm(); - task.output = `${packageManager} install ${deps.join(' ')}`; + const packageManager = getPackageManager(); + task.output = `${packageManager} add ${deps.join(' ')}`; await installDepList(dir, deps); d('installing devDependencies'); - task.output = `${packageManager} install --dev ${deps.join(' ')}`; + task.output = `${packageManager} add -D ${deps.join(' ')}`; await installDepList(dir, devDeps, DepType.DEV); d('installing exact devDependencies'); for (const packageName of exactDevDeps) { - task.output = `${packageManager} install --dev --exact ${packageName}`; + task.output = `${packageManager} add -D -E ${packageName}`; await installDepList(dir, [packageName], DepType.DEV, DepVersionRestriction.EXACT); } }; diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index 03a7093db7..ab276a4ea7 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { safeYarnOrNpm } from '@electron-forge/core-utils'; +import { getPackageManager } from '@electron-forge/core-utils'; import { ForgeTemplate } from '@electron-forge/shared-types'; import debug from 'debug'; import { Listr } from 'listr2'; @@ -56,7 +56,7 @@ async function validateTemplate(template: string, templateModule: ForgeTemplate) export default async ({ dir = process.cwd(), interactive = false, copyCIFiles = false, force = false, template = 'base' }: InitOptions): Promise => { d(`Initializing in: ${dir}`); - const packageManager = safeYarnOrNpm(); + const packageManager = getPackageManager(); const runner = new Listr<{ templateModule: ForgeTemplate; @@ -105,7 +105,7 @@ export default async ({ dir = process.cwd(), interactive = false, copyCIFiles = task: async (_, task) => { d('installing dependencies'); if (templateModule.dependencies?.length) { - task.output = `${packageManager} install ${templateModule.dependencies.join(' ')}`; + task.output = `${packageManager} add ${templateModule.dependencies.join(' ')}`; } return await installDepList(dir, templateModule.dependencies || [], DepType.PROD, DepVersionRestriction.RANGE); }, @@ -116,7 +116,7 @@ export default async ({ dir = process.cwd(), interactive = false, copyCIFiles = task: async (_, task) => { d('installing devDependencies'); if (templateModule.devDependencies?.length) { - task.output = `${packageManager} install --dev ${templateModule.devDependencies.join(' ')}`; + task.output = `${packageManager} add -D ${templateModule.devDependencies.join(' ')}`; } await installDepList(dir, templateModule.devDependencies || [], DepType.DEV); }, diff --git a/packages/api/core/src/util/index.ts b/packages/api/core/src/util/index.ts index bf55a7b631..afc0a4bfd3 100644 --- a/packages/api/core/src/util/index.ts +++ b/packages/api/core/src/util/index.ts @@ -1,4 +1,4 @@ -import { getElectronVersion, hasYarn, yarnOrNpmSpawn } from '@electron-forge/core-utils'; +import { getElectronVersion, getPackageManager, isNpm, isPnpm, isYarn, packageManagerSpawn } from '@electron-forge/core-utils'; import { BuildIdentifierConfig, BuildIdentifierMap, fromBuildIdentifier } from './forge-config'; @@ -16,7 +16,13 @@ export default class ForgeUtils { getElectronVersion = getElectronVersion; - hasYarn = hasYarn; + getPackageManager = getPackageManager; - yarnOrNpmSpawn = yarnOrNpmSpawn; + packageManagerSpawn = packageManagerSpawn; + + isNpm = isNpm; + + isYarn = isYarn; + + isPnpm = isPnpm; } diff --git a/packages/api/core/src/util/install-dependencies.ts b/packages/api/core/src/util/install-dependencies.ts index cbc61b0a72..fce44fbe6f 100644 --- a/packages/api/core/src/util/install-dependencies.ts +++ b/packages/api/core/src/util/install-dependencies.ts @@ -1,4 +1,4 @@ -import { hasYarn, yarnOrNpmSpawn } from '@electron-forge/core-utils'; +import { isNpm, isPnpm, isYarn, packageManagerSpawn } from '@electron-forge/core-utils'; import { ExitError } from '@malept/cross-spawn-promise'; import debug from 'debug'; @@ -15,24 +15,32 @@ export enum DepVersionRestriction { } export default async (dir: string, deps: string[], depType = DepType.PROD, versionRestriction = DepVersionRestriction.RANGE): Promise => { - d('installing', JSON.stringify(deps), 'in:', dir, `depType=${depType},versionRestriction=${versionRestriction},withYarn=${hasYarn()}`); + const _isNpm = await isNpm(); + const _isYarn = await isYarn(); + const _isPnpm = await isPnpm(); + d( + 'installing', + JSON.stringify(deps), + 'in:', + dir, + `depType=${depType},versionRestriction=${versionRestriction}},withNpm=${_isNpm},withYarn=${_isYarn},withPnpm=${_isPnpm}` + ); if (deps.length === 0) { d('nothing to install, stopping immediately'); return Promise.resolve(); } - let cmd = ['install'].concat(deps); - if (hasYarn()) { - cmd = ['add'].concat(deps); - if (depType === DepType.DEV) cmd.push('--dev'); - if (versionRestriction === DepVersionRestriction.EXACT) cmd.push('--exact'); - } else { - if (versionRestriction === DepVersionRestriction.EXACT) cmd.push('--save-exact'); - if (depType === DepType.DEV) cmd.push('--save-dev'); - if (depType === DepType.PROD) cmd.push('--save'); - } + /** + * To install the specified packages as dependencies + * yarn and pnpm use `add` command + * npm use `add` as an alias command of `install` + * for consistency, we use `add` command here + */ + const cmd = ['add'].concat(deps); + if (depType === DepType.DEV) cmd.push('-D'); + if (versionRestriction === DepVersionRestriction.EXACT) cmd.push('-E'); d('executing', JSON.stringify(cmd), 'in:', dir); try { - await yarnOrNpmSpawn(cmd, { + await packageManagerSpawn(cmd, { cwd: dir, stdio: 'pipe', }); diff --git a/packages/api/core/test/fast/install-dependencies_spec.ts b/packages/api/core/test/fast/install-dependencies_spec.ts index 425ed649c8..dc3ce44ecb 100644 --- a/packages/api/core/test/fast/install-dependencies_spec.ts +++ b/packages/api/core/test/fast/install-dependencies_spec.ts @@ -7,7 +7,9 @@ import installDependencies, { DepType, DepVersionRestriction } from '../../src/u describe('Install dependencies', () => { let install: typeof installDependencies; let spawnSpy: SinonStub; - let hasYarnSpy: SinonStub; + let isNpmSpy: SinonStub; + let isYarnSpy: SinonStub; + let isPnpmSpy: SinonStub; let spawnPromise: Promise; let spawnPromiseResolve: () => void; let spawnPromiseReject: () => void; @@ -19,11 +21,16 @@ describe('Install dependencies', () => { spawnPromiseReject = reject; }); spawnSpy.returns(spawnPromise); - hasYarnSpy = stub(); + isNpmSpy = stub(); + isYarnSpy = stub(); + isPnpmSpy = stub(); + install = proxyquire.noCallThru().load('../../src/util/install-dependencies', { '@electron-forge/core-utils': { - yarnOrNpmSpawn: spawnSpy, - hasYarn: hasYarnSpy, + packageManagerSpawn: spawnSpy, + isNpm: isNpmSpy, + isYarn: isYarnSpy, + isPnpm: isPnpmSpy, }, }).default; }); @@ -45,55 +52,90 @@ describe('Install dependencies', () => { await expectPromise; }); + describe('with npm', () => { + beforeEach(() => { + spawnPromiseResolve(); + isNpmSpy.resolves(true); + isYarnSpy.resolves(false); + isPnpmSpy.resolves(false); + }); + + it('should install prod deps', async () => { + await install('mydir', ['react']); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'react']); + }); + + it('should install dev deps', async () => { + await install('mydir', ['eslint'], DepType.DEV); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'eslint', '-D']); + }); + + it('should install exact deps', async () => { + await install('mydir', ['react-dom'], DepType.PROD, DepVersionRestriction.EXACT); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'react-dom', '-E']); + }); + + it('should install exact dev deps', async () => { + await install('mydir', ['mocha'], DepType.DEV, DepVersionRestriction.EXACT); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'mocha', '-D', '-E']); + }); + }); + describe('with yarn', () => { beforeEach(() => { - hasYarnSpy.returns(true); + spawnPromiseResolve(); + isNpmSpy.resolves(false); + isYarnSpy.resolves(true); + isPnpmSpy.resolves(false); }); - it('should install prod deps', () => { - install('mydir', ['react']); + it('should install prod deps', async () => { + await install('mydir', ['react']); expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'react']); }); - it('should install dev deps', () => { - install('mydir', ['eslint'], DepType.DEV); - expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'eslint', '--dev']); + it('should install dev deps', async () => { + await install('mydir', ['eslint'], DepType.DEV); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'eslint', '-D']); }); - it('should install exact deps', () => { - install('mydir', ['react-dom'], DepType.PROD, DepVersionRestriction.EXACT); - expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'react-dom', '--exact']); + it('should install exact deps', async () => { + await install('mydir', ['react-dom'], DepType.PROD, DepVersionRestriction.EXACT); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'react-dom', '-E']); }); - it('should install exact dev deps', () => { - install('mydir', ['mocha'], DepType.DEV, DepVersionRestriction.EXACT); - expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'mocha', '--dev', '--exact']); + it('should install exact dev deps', async () => { + await install('mydir', ['mocha'], DepType.DEV, DepVersionRestriction.EXACT); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'mocha', '-D', '-E']); }); }); - describe('with npm', () => { + describe('with pnpm', () => { beforeEach(() => { - hasYarnSpy.returns(false); + spawnPromiseResolve(); + isNpmSpy.resolves(false); + isYarnSpy.resolves(false); + isPnpmSpy.resolves(true); }); - it('should install prod deps', () => { - install('mydir', ['react']); - expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['install', 'react', '--save']); + it('should install prod deps', async () => { + await install('mydir', ['react']); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'react']); }); - it('should install dev deps', () => { - install('mydir', ['eslint'], DepType.DEV); - expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['install', 'eslint', '--save-dev']); + it('should install dev deps', async () => { + await install('mydir', ['eslint'], DepType.DEV); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'eslint', '-D']); }); - it('should install exact deps', () => { - install('mydir', ['react-dom'], DepType.PROD, DepVersionRestriction.EXACT); - expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['install', 'react-dom', '--save-exact', '--save']); + it('should install exact deps', async () => { + await install('mydir', ['react-dom'], DepType.PROD, DepVersionRestriction.EXACT); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'react-dom', '-E']); }); - it('should install exact dev deps', () => { - install('mydir', ['mocha'], DepType.DEV, DepVersionRestriction.EXACT); - expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['install', 'mocha', '--save-exact', '--save-dev']); + it('should install exact dev deps', async () => { + await install('mydir', ['mocha'], DepType.DEV, DepVersionRestriction.EXACT); + expect(spawnSpy.firstCall.args[0]).to.be.deep.equal(['add', 'mocha', '-D', '-E']); }); }); }); diff --git a/packages/api/core/test/fixture/pnpm-workspace/node_modules/electron/package.json b/packages/api/core/test/fixture/pnpm-workspace/node_modules/electron/package.json new file mode 100644 index 0000000000..6af1f25522 --- /dev/null +++ b/packages/api/core/test/fixture/pnpm-workspace/node_modules/electron/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron", + "version": "4.0.9" +} diff --git a/packages/api/core/test/fixture/pnpm-workspace/packages/electron-folder-in-node-modules/.gitkeep b/packages/api/core/test/fixture/pnpm-workspace/packages/electron-folder-in-node-modules/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/api/core/test/fixture/pnpm-workspace/packages/electron-folder-in-node-modules/node_modules/electron/package.json b/packages/api/core/test/fixture/pnpm-workspace/packages/electron-folder-in-node-modules/node_modules/electron/package.json new file mode 100644 index 0000000000..c57f23c7a7 --- /dev/null +++ b/packages/api/core/test/fixture/pnpm-workspace/packages/electron-folder-in-node-modules/node_modules/electron/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron", + "version": "^12.0.0" +} diff --git a/packages/api/core/test/fixture/pnpm-workspace/packages/subpackage/.gitkeep b/packages/api/core/test/fixture/pnpm-workspace/packages/subpackage/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/api/core/test/fixture/pnpm-workspace/packages/with-node-modules/.gitkeep b/packages/api/core/test/fixture/pnpm-workspace/packages/with-node-modules/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/api/core/test/fixture/pnpm-workspace/packages/with-node-modules/node_modules/some-other-module-second/package.json b/packages/api/core/test/fixture/pnpm-workspace/packages/with-node-modules/node_modules/some-other-module-second/package.json new file mode 100644 index 0000000000..4b96ec936b --- /dev/null +++ b/packages/api/core/test/fixture/pnpm-workspace/packages/with-node-modules/node_modules/some-other-module-second/package.json @@ -0,0 +1,4 @@ +{ + "name": "some-other-module-second", + "version": "0.0.0" +} diff --git a/packages/api/core/test/fixture/pnpm-workspace/packages/with-node-modules/node_modules/some-other-module/package.json b/packages/api/core/test/fixture/pnpm-workspace/packages/with-node-modules/node_modules/some-other-module/package.json new file mode 100644 index 0000000000..61c4b951c5 --- /dev/null +++ b/packages/api/core/test/fixture/pnpm-workspace/packages/with-node-modules/node_modules/some-other-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "some-other-module-one", + "version": "0.0.0" +} diff --git a/packages/api/core/test/fixture/pnpm-workspace/pnpm-lock.yaml b/packages/api/core/test/fixture/pnpm-workspace/pnpm-lock.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/api/core/test/fixture/pnpm-workspace/pnpm-workspace.yaml b/packages/api/core/test/fixture/pnpm-workspace/pnpm-workspace.yaml new file mode 100644 index 0000000000..18ec407efc --- /dev/null +++ b/packages/api/core/test/fixture/pnpm-workspace/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/packages/api/core/test/slow/api_spec_slow.ts b/packages/api/core/test/slow/api_spec_slow.ts index e30683ba20..03d6fb1d0e 100644 --- a/packages/api/core/test/slow/api_spec_slow.ts +++ b/packages/api/core/test/slow/api_spec_slow.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import { execSync } from 'child_process'; import path from 'path'; -import { yarnOrNpmSpawn } from '@electron-forge/core-utils'; +import { packageManagerSpawn } from '@electron-forge/core-utils'; import { createDefaultCertificate } from '@electron-forge/maker-appx'; import { ForgeConfig, IForgeResolvableMaker } from '@electron-forge/shared-types'; import { ensureTestDirIsNonexistent, expectLintToPass, expectProjectPathExists } from '@electron-forge/test-utils'; @@ -35,13 +35,13 @@ async function updatePackageJSON(dir: string, packageJSONUpdater: (packageJSON: await fs.writeJson(path.resolve(dir, 'package.json'), packageJSON); } -for (const nodeInstaller of ['npm', 'yarn']) { +for (const nodeInstaller of ['npm', 'pnpm', 'yarn']) { process.env.NODE_INSTALLER = nodeInstaller; describe(`electron-forge API (with installer=${nodeInstaller})`, () => { let dir: string; before(async () => { - await yarnOrNpmSpawn(['link:prepare']); + await packageManagerSpawn(['link:prepare']); }); const beforeInitTest = (params?: Partial, beforeInit?: BeforeInitFunction) => { @@ -199,7 +199,7 @@ for (const nodeInstaller of ['npm', 'yarn']) { before(async () => { dir = await ensureTestDirIsNonexistent(); await fs.mkdir(dir); - execSync(`${nodeInstaller} init -y`, { + execSync(nodeInstaller === 'pnpm' ? `${nodeInstaller} init` : `${nodeInstaller} init -y`, { cwd: dir, }); }); @@ -224,7 +224,7 @@ for (const nodeInstaller of ['npm', 'yarn']) { }); after(async () => { - await yarnOrNpmSpawn(['link:remove']); + await packageManagerSpawn(['link:remove']); }); }); } @@ -233,7 +233,7 @@ describe('Electron Forge API', () => { let dir: string; before(async () => { - await yarnOrNpmSpawn(['link:prepare']); + await packageManagerSpawn(['link:prepare']); }); describe('after init', () => { @@ -499,6 +499,6 @@ describe('Electron Forge API', () => { }); after(async () => { - await yarnOrNpmSpawn(['link:remove']); + await packageManagerSpawn(['link:remove']); }); }); diff --git a/packages/template/vite-typescript/test/ViteTypeScriptTemplate_spec_slow.ts b/packages/template/vite-typescript/test/ViteTypeScriptTemplate_spec_slow.ts index 565769df86..a6f4c8b09b 100644 --- a/packages/template/vite-typescript/test/ViteTypeScriptTemplate_spec_slow.ts +++ b/packages/template/vite-typescript/test/ViteTypeScriptTemplate_spec_slow.ts @@ -1,7 +1,7 @@ import cp from 'child_process'; import path from 'path'; -import { yarnOrNpmSpawn } from '@electron-forge/core-utils'; +import { packageManagerSpawn } from '@electron-forge/core-utils'; import * as testUtils from '@electron-forge/test-utils'; import { expect } from 'chai'; import glob from 'fast-glob'; @@ -14,12 +14,12 @@ describe('ViteTypeScriptTemplate', () => { let dir: string; before(async () => { - await yarnOrNpmSpawn(['link:prepare']); + await packageManagerSpawn(['link:prepare']); dir = await testUtils.ensureTestDirIsNonexistent(); }); after(async () => { - await yarnOrNpmSpawn(['link:remove']); + await packageManagerSpawn(['link:remove']); await killWindowsEsbuildExe(); await fs.remove(dir); }); @@ -80,7 +80,7 @@ describe('ViteTypeScriptTemplate', () => { vite: `${require('../../../../node_modules/vite/package.json').version}`, }; await fs.writeJson(path.resolve(dir, 'package.json'), pj); - await yarnOrNpmSpawn(['install'], { + await packageManagerSpawn(['install'], { cwd: dir, }); diff --git a/packages/template/webpack-typescript/test/WebpackTypeScript_spec_slow.ts b/packages/template/webpack-typescript/test/WebpackTypeScript_spec_slow.ts index bb78fae16c..1496b16d14 100644 --- a/packages/template/webpack-typescript/test/WebpackTypeScript_spec_slow.ts +++ b/packages/template/webpack-typescript/test/WebpackTypeScript_spec_slow.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { yarnOrNpmSpawn } from '@electron-forge/core-utils'; +import { packageManagerSpawn } from '@electron-forge/core-utils'; import * as testUtils from '@electron-forge/test-utils'; import { expect } from 'chai'; import glob from 'fast-glob'; @@ -13,7 +13,7 @@ describe('WebpackTypeScriptTemplate', () => { let dir: string; before(async () => { - await yarnOrNpmSpawn(['link:prepare']); + await packageManagerSpawn(['link:prepare']); dir = await testUtils.ensureTestDirIsNonexistent(); }); @@ -73,7 +73,7 @@ describe('WebpackTypeScriptTemplate', () => { webpack: `${require('../../../../node_modules/webpack/package.json').version}`, }; await fs.writeJson(path.resolve(dir, 'package.json'), pj); - await yarnOrNpmSpawn(['install'], { + await packageManagerSpawn(['install'], { cwd: dir, }); @@ -96,7 +96,7 @@ describe('WebpackTypeScriptTemplate', () => { }); after(async () => { - await yarnOrNpmSpawn(['link:remove']); + await packageManagerSpawn(['link:remove']); await fs.remove(dir); }); }); diff --git a/packages/utils/core-utils/package.json b/packages/utils/core-utils/package.json index 8c12d6495d..a2290eae42 100644 --- a/packages/utils/core-utils/package.json +++ b/packages/utils/core-utils/package.json @@ -13,11 +13,11 @@ "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.3.1", + "detect-package-manager": "^3.0.1", "find-up": "^5.0.0", "fs-extra": "^10.0.0", "log-symbols": "^4.0.0", - "semver": "^7.2.1", - "yarn-or-npm": "^3.0.1" + "semver": "^7.2.1" }, "engines": { "node": ">= 14.17.5" diff --git a/packages/utils/core-utils/src/electron-version.ts b/packages/utils/core-utils/src/electron-version.ts index 2709800a98..2afd970162 100644 --- a/packages/utils/core-utils/src/electron-version.ts +++ b/packages/utils/core-utils/src/electron-version.ts @@ -5,7 +5,7 @@ import findUp from 'find-up'; import fs from 'fs-extra'; import semver from 'semver'; -import { safeYarnOrNpm } from './yarn-or-npm'; +import { getPackageManager } from './package-manager'; const d = debug('electron-forge:electron-version'); @@ -44,7 +44,7 @@ async function determineNodeModulesPath(dir: string, packageName: string): Promi export class PackageNotFoundError extends Error { constructor(packageName: string, dir: string) { - super(`Cannot find the package "${packageName}". Perhaps you need to run "${safeYarnOrNpm()} install" in "${dir}"?`); + super(`Cannot find the package "${packageName}". Perhaps you need to run "${getPackageManager()} install" in "${dir}"?`); } } diff --git a/packages/utils/core-utils/src/index.ts b/packages/utils/core-utils/src/index.ts index 2b4f1876ef..e5b67335f6 100644 --- a/packages/utils/core-utils/src/index.ts +++ b/packages/utils/core-utils/src/index.ts @@ -1,3 +1,3 @@ export * from './rebuild'; export * from './electron-version'; -export * from './yarn-or-npm'; +export * from './package-manager'; diff --git a/packages/utils/core-utils/src/package-manager.ts b/packages/utils/core-utils/src/package-manager.ts new file mode 100644 index 0000000000..d3a14a5576 --- /dev/null +++ b/packages/utils/core-utils/src/package-manager.ts @@ -0,0 +1,46 @@ +import { CrossSpawnArgs, CrossSpawnOptions, spawn } from '@malept/cross-spawn-promise'; +import chalk from 'chalk'; +import { detect } from 'detect-package-manager'; +import logSymbols from 'log-symbols'; + +export type PackageManager = 'npm' | 'yarn' | 'pnpm'; + +export const getPackageManager = async (): Promise => { + const detectedPackageManager = await detect(); + const installer = process.env.NODE_INSTALLER || detectedPackageManager; + if (!process.env.NODE_INSTALLER) { + console.warn(logSymbols.warning, chalk.yellow(`Unknown NODE_INSTALLER env, using detected installer ${installer}`)); + } + return installer as PackageManager; +}; + +export const packageManagerSpawn = async (args?: CrossSpawnArgs, opts?: CrossSpawnOptions): Promise => { + const pm = await getPackageManager(); + return spawn(pm, args, opts); +}; + +const cacheWrap = (fn: () => Promise) => { + const cache = new Map(); + return async (key: string): Promise => { + if (cache.has(key)) { + return cache.get(key); + } + const pm = await fn(); + cache.set(key, pm === key); + return pm === key; + }; +}; + +const _pm = cacheWrap(getPackageManager); + +export const isNpm = async () => { + return await _pm('npm'); +}; + +export const isYarn = async () => { + return await _pm('yarn'); +}; + +export const isPnpm = async () => { + return await _pm('pnpm'); +}; diff --git a/packages/utils/core-utils/src/yarn-or-npm.ts b/packages/utils/core-utils/src/yarn-or-npm.ts deleted file mode 100644 index a2ddb4d259..0000000000 --- a/packages/utils/core-utils/src/yarn-or-npm.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CrossSpawnArgs, CrossSpawnOptions, spawn } from '@malept/cross-spawn-promise'; -import chalk from 'chalk'; -import logSymbols from 'log-symbols'; -import yarnOrNpm from 'yarn-or-npm'; - -export const safeYarnOrNpm = () => { - const system = yarnOrNpm(); - switch (process.env.NODE_INSTALLER) { - case 'yarn': - case 'npm': - return process.env.NODE_INSTALLER; - default: - if (process.env.NODE_INSTALLER) { - console.warn(logSymbols.warning, chalk.yellow(`Unknown NODE_INSTALLER, using detected installer ${system}`)); - } - return system; - } -}; - -export const yarnOrNpmSpawn = (args?: CrossSpawnArgs, opts?: CrossSpawnOptions): Promise => spawn(safeYarnOrNpm(), args, opts); - -export const hasYarn = (): boolean => safeYarnOrNpm() === 'yarn'; diff --git a/packages/utils/core-utils/test/electron-version_spec.ts b/packages/utils/core-utils/test/electron-version_spec.ts index 26838e426d..5e968d2c13 100644 --- a/packages/utils/core-utils/test/electron-version_spec.ts +++ b/packages/utils/core-utils/test/electron-version_spec.ts @@ -100,6 +100,25 @@ describe('getElectronVersion', () => { delete process.env.NODE_INSTALLER; }); }); + + describe('with pnpm workspaces', () => { + before(() => { + process.env.NODE_INSTALLER = 'pnpm'; + }); + + it('works with a non-exact version', async () => { + const fixtureDir = path.resolve(fixturePath, 'pnpm-workspace', 'packages', 'subpackage'); + const packageJSON = { + devDependencies: { electron: '^4.0.4' }, + }; + + expect(await getElectronVersion(fixtureDir, packageJSON)).to.be.equal('4.0.9'); + }); + + after(() => { + delete process.env.NODE_INSTALLER; + }); + }); }); describe('getElectronModulePath', () => { @@ -192,4 +211,45 @@ describe('getElectronModulePath', () => { delete process.env.NODE_INSTALLER; }); }); + + describe('with pnpm workspaces', () => { + before(() => { + process.env.NODE_INSTALLER = 'pnpm'; + }); + + it('finds the top-level electron module', async () => { + const workspaceDir = path.resolve(fixturePath, 'pnpm-workspace'); + const fixtureDir = path.join(workspaceDir, 'packages', 'subpackage'); + const packageJSON = { + devDependencies: { electron: '^4.0.4' }, + }; + + expect(await getElectronModulePath(fixtureDir, packageJSON)).to.be.equal(path.join(workspaceDir, 'node_modules', 'electron')); + }); + + it('finds the top-level electron module despite the additional node_modules folder inside the package', async () => { + const workspaceDir = path.resolve(fixturePath, 'pnpm-workspace'); + const fixtureDir = path.join(workspaceDir, 'packages', 'with-node-modules'); + const packageJSON = { + devDependencies: { electron: '^4.0.4' }, + }; + + expect(await getElectronModulePath(fixtureDir, packageJSON)).to.be.equal(path.join(workspaceDir, 'node_modules', 'electron')); + }); + + it('finds the correct electron module in nohoist mode', async () => { + const workspaceDir = path.resolve(fixturePath, 'pnpm-workspace'); + const fixtureDir = path.join(workspaceDir, 'packages', 'electron-folder-in-node-modules'); + const packageJSON = { + devDependencies: { electron: '^13.0.0' }, + }; + + expect(await getElectronModulePath(fixtureDir, packageJSON)).to.be.equal(path.join(fixtureDir, 'node_modules', 'electron')); + expect(await getElectronModulePath(fixtureDir, packageJSON)).not.to.be.equal(path.join(workspaceDir, 'node_modules', 'electron')); + }); + + after(() => { + delete process.env.NODE_INSTALLER; + }); + }); }); diff --git a/packages/utils/core-utils/test/package-manager_spec.ts b/packages/utils/core-utils/test/package-manager_spec.ts new file mode 100644 index 0000000000..ab287d9802 --- /dev/null +++ b/packages/utils/core-utils/test/package-manager_spec.ts @@ -0,0 +1,56 @@ +import { expect } from 'chai'; + +import { getPackageManager, isNpm, isPnpm, isYarn } from '../src/package-manager'; + +describe('checkPackageManager', () => { + let nodeInstaller: string | undefined; + + beforeEach(() => { + nodeInstaller = process.env.NODE_INSTALLER; + delete process.env.NODE_INSTALLER; + }); + + afterEach(() => { + if (!nodeInstaller) { + delete process.env.NODE_INSTALLER; + } else { + process.env.NODE_INSTALLER = nodeInstaller; + } + }); + + it('should return npm if NODE_INSTALLER=npm', async () => { + process.env.NODE_INSTALLER = 'npm'; + const pm = await getPackageManager(); + expect(pm).to.be.equal('npm'); + }); + + it('should return yarn if NODE_INSTALLER=yarn', async () => { + process.env.NODE_INSTALLER = 'yarn'; + const pm = await getPackageManager(); + expect(pm).to.be.equal('yarn'); + }); + + it('should return yarn if NODE_INSTALLER=pnpm', async () => { + process.env.NODE_INSTALLER = 'pnpm'; + const pm = await getPackageManager(); + expect(pm).to.be.equal('pnpm'); + }); + + it('function `isNpm` should return true if NODE_INSTALLER=npm', async () => { + process.env.NODE_INSTALLER = 'npm'; + const res = await isNpm(); + expect(res).to.be.equal(true); + }); + + it('function `isYarn` should return true if NODE_INSTALLER=yarn', async () => { + process.env.NODE_INSTALLER = 'yarn'; + const res = await isYarn(); + expect(res).to.be.equal(true); + }); + + it('function `isPnpm` should return true if NODE_INSTALLER=pnpm', async () => { + process.env.NODE_INSTALLER = 'pnpm'; + const res = await isPnpm(); + expect(res).to.be.equal(true); + }); +}); diff --git a/packages/utils/core-utils/test/yarn-or-npm_spec.ts b/packages/utils/core-utils/test/yarn-or-npm_spec.ts deleted file mode 100644 index 8be7ecc9d6..0000000000 --- a/packages/utils/core-utils/test/yarn-or-npm_spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { expect } from 'chai'; -import systemYarnOrNpm from 'yarn-or-npm'; - -import { safeYarnOrNpm } from '../src/yarn-or-npm'; - -describe('yarn-or-npm', () => { - let nodeInstaller: string | undefined; - - beforeEach(() => { - nodeInstaller = process.env.NODE_INSTALLER; - delete process.env.NODE_INSTALLER; - }); - - afterEach(() => { - if (!nodeInstaller) { - delete process.env.NODE_INSTALLER; - } else { - process.env.NODE_INSTALLER = nodeInstaller; - } - }); - - it('should by default equal the system yarn-or-npm value', () => { - expect(safeYarnOrNpm()).to.be.equal(systemYarnOrNpm()); - }); - - it('should return yarn if NODE_INSTALLER=yarn', () => { - process.env.NODE_INSTALLER = 'yarn'; - expect(safeYarnOrNpm()).to.be.equal('yarn'); - }); - - it('should return npm if NODE_INSTALLER=npm', () => { - process.env.NODE_INSTALLER = 'npm'; - expect(safeYarnOrNpm()).to.be.equal('npm'); - }); - - it('should return system value if NODE_INSTALLER is an unrecognized installer', () => { - process.env.NODE_INSTALLER = 'magical_unicorn'; - expect(safeYarnOrNpm()).to.be.equal(systemYarnOrNpm()); - }); -}); diff --git a/packages/utils/test-utils/src/index.ts b/packages/utils/test-utils/src/index.ts index df03c252d4..417f35d8b4 100644 --- a/packages/utils/test-utils/src/index.ts +++ b/packages/utils/test-utils/src/index.ts @@ -15,7 +15,7 @@ export async function runNPMInstall(dir: string, ...args: string[]) { export async function ensureModulesInstalled(dir: string, deps: string[], devDeps: string[]): Promise { await runNPMInstall(dir, ...deps); - await runNPMInstall(dir, '--save-dev', ...devDeps); + await runNPMInstall(dir, '-D', ...devDeps); } let dirID = Date.now(); diff --git a/typings/yarn-or-npm/index.d.ts b/typings/yarn-or-npm/index.d.ts deleted file mode 100644 index f80008235e..0000000000 --- a/typings/yarn-or-npm/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'yarn-or-npm' { - const yon: () => 'yarn' | 'npm'; - export default yon; -} diff --git a/yarn.lock b/yarn.lock index 50a9ca41a9..6c04a0dbd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4785,7 +4785,7 @@ cross-spawn-windows-exe@^1.1.0, cross-spawn-windows-exe@^1.2.0: is-wsl "^2.2.0" which "^2.0.2" -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -5049,6 +5049,13 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +detect-package-manager@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/detect-package-manager/-/detect-package-manager-3.0.1.tgz#ec9689915b47e2ecf3774118849bc7033f0a2151" + integrity sha512-qoHDH6+lMcpJPAScE7+5CYj91W0mxZNXTwZPrCqi1KMk+x+AoQScQ2V1QyqTln1rHU5Haq5fikvOGHv+leKD8A== + dependencies: + execa "^5.1.1" + dezalgo@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" @@ -12699,14 +12706,6 @@ yargs@^17.6.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yarn-or-npm@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/yarn-or-npm/-/yarn-or-npm-3.0.1.tgz#6336eea4dff7e23e226acc98c1a8ada17a1b8666" - integrity sha512-fTiQP6WbDAh5QZAVdbMQkecZoahnbOjClTQhzv74WX5h2Uaidj1isf9FDes11TKtsZ0/ZVfZsqZ+O3x6aLERHQ== - dependencies: - cross-spawn "^6.0.5" - pkg-dir "^4.2.0" - yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"