Skip to content
Merged
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: 21 additions & 0 deletions packages/api/cli/spec/util/check-system.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ describe('checkPackageManager', () => {
);
});

it('should throw if using yarn without node-linker=node-modules', async () => {
vi.mocked(resolvePackageManager).mockResolvedValue({
executable: 'yarn',
install: 'add',
dev: '--dev',
exact: '--exact',
});
vi.mocked(spawnPackageManager).mockImplementation((_pm, args) => {
if (args?.join(' ') === 'config get nodeLinker') {
return Promise.resolve('isolated');
} else if (args?.join(' ') === '--version') {
return Promise.resolve('4.1.0');
} else {
throw new Error('Unexpected command');
}
});
await expect(checkPackageManager()).rejects.toThrow(
'When using Yarn 2+, `nodeLinker` must be set to "node-modules". Run `yarn config set nodeLinker node-modules` to set this config value, or add it to your project\'s `.yarnrc` file.',
);
});

it.each(['hoist-pattern', 'public-hoist-pattern'])(
'should pass without validation if user has set %s in their pnpm config',
async (cfg) => {
Expand Down
21 changes: 21 additions & 0 deletions packages/api/cli/src/util/check-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@ async function checkPnpmConfig() {
}
}

async function checkYarnConfig() {
const { yarn } = PACKAGE_MANAGERS;
const yarnVersion = await spawnPackageManager(yarn, ['--version']);
const nodeLinker = await spawnPackageManager(yarn, [
'config',
'get',
'nodeLinker',
]);
if (
yarnVersion &&
semver.gte(yarnVersion, '2.0.0') &&
nodeLinker !== 'node-modules'
) {
throw new Error(
'When using Yarn 2+, `nodeLinker` must be set to "node-modules". Run `yarn config set nodeLinker node-modules` to set this config value, or add it to your project\'s `.yarnrc` file.',
);
}
}

// TODO(erickzhao): Drop antiquated versions of npm for Forge v8
const ALLOWLISTED_VERSIONS: Record<
SupportedPackageManager,
Expand Down Expand Up @@ -108,6 +127,8 @@ export async function checkPackageManager() {

if (pm.executable === 'pnpm') {
await checkPnpmConfig();
} else if (pm.executable === 'yarn') {
await checkYarnConfig();
}

return `${pm.executable}@${versionString}`;
Expand Down
1 change: 1 addition & 0 deletions packages/template/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@malept/cross-spawn-promise": "^2.0.0",
"debug": "^4.3.1",
"fs-extra": "^10.0.0",
"semver": "^7.2.1",
"username": "^5.1.0"
},
"devDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions packages/template/base/src/BaseTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@electron-forge/shared-types';
import debug from 'debug';
import fs from 'fs-extra';
import semver from 'semver';

import determineAuthor from './determine-author';

Expand Down Expand Up @@ -71,6 +72,13 @@ export class BaseTemplate implements ForgeTemplate {

if (pm.executable === 'pnpm') {
rootFiles.push('_npmrc');
} else if (
// Support Yarn 2+ by default by initializing with nodeLinker: node-modules
pm.executable === 'yarn' &&
pm.version &&
semver.gte(pm.version, '2.0.0')
) {
rootFiles.push('_yarnrc');
}

if (copyCIFiles) {
Expand Down
1 change: 1 addition & 0 deletions packages/template/base/tmpl/_yarnrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
56 changes: 34 additions & 22 deletions packages/utils/core-utils/spec/package-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ vi.mock('find-up', async (importOriginal) => {
});

describe('package-manager', () => {
describe('npm_config_user_agent', () => {
beforeAll(() => {
const originalUa = process.env.npm_config_user_agent;

return () => {
process.env.npm_config_user_agent = originalUa;
};
});
beforeAll(() => {
const originalUa = process.env.npm_config_user_agent;

return () => {
process.env.npm_config_user_agent = originalUa;
};
});
describe('npm_config_user_agent', () => {
it.each([
{
ua: 'yarn/1.22.22 npm/? node/v22.13.0 darwin arm64',
Expand Down Expand Up @@ -112,16 +111,22 @@ describe('package-manager', () => {
'should return $pm if NODE_INSTALLER=$pm',
async ({ pm }) => {
process.env.NODE_INSTALLER = pm;
vi.mocked(spawn).mockResolvedValue('9.9.9');
await expect(resolvePackageManager()).resolves.toHaveProperty(
'executable',
pm,
);
await expect(resolvePackageManager()).resolves.toHaveProperty(
'version',
'9.9.9',
);
},
);

it('should return npm if package manager is unsupported', async () => {
process.env.NODE_INSTALLER = 'bun';
console.warn = vi.fn();
vi.mocked(spawn).mockResolvedValue('1.22.22');
await expect(resolvePackageManager()).resolves.toHaveProperty(
'executable',
'npm',
Expand All @@ -133,33 +138,40 @@ describe('package-manager', () => {
});
});

describe('spawnPackageManager', () => {
it('should trim the output', async () => {
vi.mocked(spawn).mockResolvedValue(' foo \n');
const result = await spawnPackageManager({
executable: 'npm',
install: 'install',
dev: '--save-dev',
exact: '--save-exact',
});
expect(result).toBe('foo');
});
});

it('should use the package manager for the nearest ancestor lockfile if detected', async () => {
delete process.env.npm_config_user_agent;
vi.mocked(findUp).mockResolvedValue('/Users/foo/bar/yarn.lock');
vi.mocked(spawn).mockResolvedValue('1.22.22');
await expect(resolvePackageManager()).resolves.toHaveProperty(
'executable',
'yarn',
);
});

it('should fall back to npm if no other strategy worked', async () => {
process.env.npm_config_user_agent = undefined;
delete process.env.npm_config_user_agent;
vi.mocked(findUp).mockResolvedValue(undefined);
vi.mocked(spawn).mockResolvedValue('9.99.99');
await expect(resolvePackageManager()).resolves.toHaveProperty(
'executable',
'npm',
);
await expect(resolvePackageManager()).resolves.toHaveProperty(
'version',
'9.99.99',
);
});

describe('spawnPackageManager', () => {
it('should trim the output', async () => {
vi.mocked(spawn).mockResolvedValue(' foo \n');
const result = await spawnPackageManager({
executable: 'npm',
install: 'install',
dev: '--save-dev',
exact: '--save-exact',
});
expect(result).toBe('foo');
});
});
});
68 changes: 46 additions & 22 deletions packages/utils/core-utils/src/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,43 @@ export const resolvePackageManager: () => Promise<PMDetails> = async () => {
const lockfileName = path.basename(lockfile);
lockfilePM = PM_FROM_LOCKFILE[lockfileName];
}
const installer =
process.env.NODE_INSTALLER || executingPM?.name || lockfilePM;

// TODO(erickzhao): Remove NODE_INSTALLER environment variable for Forge 8
if (typeof process.env.NODE_INSTALLER === 'string' && !hasWarned) {
console.warn(
logSymbols.warning,
chalk.yellow(
`The NODE_INSTALLER environment variable is deprecated and will be removed in Electron Forge v8`,
),
let installer;
let installerVersion;

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(
PACKAGE_MANAGERS[installer as SupportedPackageManager],
['--version'],
);
if (!hasWarned) {
console.warn(
logSymbols.warning,
chalk.yellow(
`The NODE_INSTALLER environment variable is deprecated and will be removed in Electron Forge v8`,
),
);
hasWarned = true;
}
} else {
console.warn(
logSymbols.warning,
chalk.yellow(
`Package manager ${chalk.red(process.env.NODE_INSTALLER)} is unsupported. Falling back to ${chalk.green('npm')} instead.`,
),
);
}
} else if (executingPM) {
installer = executingPM.name;
installerVersion = executingPM.version;
} else if (lockfilePM) {
installer = lockfilePM;
installerVersion = await spawnPackageManager(
PACKAGE_MANAGERS[installer as SupportedPackageManager],
['--version'],
);
hasWarned = true;
}

switch (installer) {
Expand All @@ -117,19 +142,18 @@ export const resolvePackageManager: () => Promise<PMDetails> = async () => {
d(
`Resolved package manager to ${installer}. (Derived from NODE_INSTALLER: ${process.env.NODE_INSTALLER}, npm_config_user_agent: ${process.env.npm_config_user_agent}, lockfile: ${lockfilePM})`,
);
return { ...PACKAGE_MANAGERS[installer], version: executingPM?.version };
return {
...PACKAGE_MANAGERS[installer],
version: installerVersion,
};
default:
if (installer !== undefined) {
console.warn(
logSymbols.warning,
chalk.yellow(
`Package manager ${chalk.red(installer)} is unsupported. Falling back to ${chalk.green('npm')} instead.`,
),
);
} else {
d(`No package manager detected. Falling back to npm.`);
}
return PACKAGE_MANAGERS['npm'];
d(`No valid package manager detected. Falling back to npm.`);
return {
...PACKAGE_MANAGERS['npm'],
version: await spawnPackageManager(PACKAGE_MANAGERS['npm'], [
'--version',
]),
};
}
};

Expand Down
Loading