@electron-forge/cli
Version:
A complete tool for building modern Electron applications
199 lines (183 loc) • 5.78 kB
text/typescript
import { exec } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import {
PACKAGE_MANAGERS,
resolvePackageManager,
spawnPackageManager,
SupportedPackageManager,
} from '@electron-forge/core-utils';
import { ForgeListrTask } from '@electron-forge/shared-types';
import debug from 'debug';
import fs from 'fs-extra';
import semver from 'semver';
const d = debug('electron-forge:check-system');
async function getGitVersion(): Promise<string | null> {
return new Promise<string | null>((resolve) => {
exec('git --version', (err, output) =>
err
? resolve(null)
: resolve(output.toString().trim().split(' ').reverse()[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 { pnpm } = PACKAGE_MANAGERS;
const hoistPattern = await spawnPackageManager(pnpm, [
'config',
'get',
'hoist-pattern',
]);
const publicHoistPattern = await spawnPackageManager(pnpm, [
'config',
'get',
'public-hoist-pattern',
]);
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;
}
const nodeLinker = await spawnPackageManager(pnpm, [
'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.',
);
}
}
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.yml` file.',
);
}
}
// 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',
},
};
export async function checkPackageManager() {
const pm = await resolvePackageManager();
const version = pm.version ?? (await spawnPackageManager(pm, ['--version']));
const versionString = version.toString().trim();
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();
} else if (pm.executable === 'yarn') {
await checkYarnConfig();
}
return `${pm.executable}@${versionString}`;
}
/**
* Some people know their system is OK and don't appreciate the 800ms lag in
* start up that these checks (in particular the package manager check) costs.
*
* Simply creating this flag file in your home directory will skip these checks
* and shave ~800ms off your forge start time.
*
* This is specifically not documented or everyone would make it.
*/
const SKIP_SYSTEM_CHECK = path.resolve(
os.homedir(),
'.skip-forge-system-check',
);
export type SystemCheckContext = {
command: string;
git: boolean;
node: boolean;
packageManager: boolean;
};
export async function checkSystem(
callerTask: ForgeListrTask<SystemCheckContext>,
) {
if (!(await fs.pathExists(SKIP_SYSTEM_CHECK))) {
d('checking system, create ~/.skip-forge-system-check to stop doing this');
return callerTask.newListr<SystemCheckContext>(
[
{
title: 'Checking git exists',
// We only call the `initGit` helper in the `init` and `import` commands
enabled: (ctx): boolean =>
(ctx.command === 'init' || ctx.command === 'import') && ctx.git,
task: async (_, task) => {
const gitVersion = await getGitVersion();
if (gitVersion) {
task.title = `Found git@${gitVersion}`;
} else {
throw new Error('Could not find git in environment');
}
},
},
{
title: 'Checking package manager version',
task: async (_, task) => {
const packageManager = await checkPackageManager();
task.title = `Found ${packageManager}`;
},
},
],
{
concurrent: true,
exitOnError: true,
rendererOptions: {
collapseSubtasks: true,
},
},
);
}
d('skipping system check');
return true;
}