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

refactor: use listr2 instead of async-ora #3022

Merged
merged 11 commits into from
Oct 31, 2022
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
"generate-changelog": "^1.8.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"listr2": "^4.0.4",
"listr2": "^5.0.3",
"minimist": "^1.2.6",
"mocha": "^9.0.1",
"nyc": "^15.1.0",
Expand Down
1 change: 1 addition & 0 deletions packages/api/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"debug": "^4.3.1",
"fs-extra": "^10.0.0",
"inquirer": "^8.0.0",
"listr2": "^5.0.3",
"semver": "^7.2.1"
},
"engines": {
Expand Down
28 changes: 20 additions & 8 deletions packages/api/cli/src/electron-forge.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/usr/bin/env node
// This file requires a shebang above. If it is missing, this is an error.

import { asyncOra } from '@electron-forge/async-ora';
import chalk from 'chalk';
import program from 'commander';
import { Listr } from 'listr2';

import './util/terminate';

import checkSystem from './util/check-system';
import { checkSystem } from './util/check-system';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const metadata = require('../package.json');
Expand Down Expand Up @@ -50,14 +50,26 @@ program
});

(async () => {
let goodSystem;
await asyncOra('Checking your system', async (ora) => {
goodSystem = await checkSystem(ora);
});
const runner = new Listr<never>(
[
{
title: 'Checking your system',
task: async (_, task) => {
return await checkSystem(task);
},
},
],
{
concurrent: false,
exitOnError: false,
}
);

await runner.run();

if (!goodSystem) {
if (runner.err.length) {
console.error(
chalk.red(`It looks like you are missing some dependencies you need to get Electron running.
chalk.red(`\nIt looks like you are missing some dependencies you need to get Electron running.
Make sure you have git installed and Node.js version ${metadata.engines.node}`)
);
process.exit(1);
Expand Down
103 changes: 70 additions & 33 deletions packages/api/cli/src/util/check-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,70 @@ import { exec } from 'child_process';
import os from 'os';
import path from 'path';

import { OraImpl } from '@electron-forge/async-ora';
import { utils as forgeUtils } from '@electron-forge/core';
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 checkGitExists() {
return new Promise<boolean>((resolve) => {
exec('git --version', (err) => resolve(!err));
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])));
});
}

async function checkNodeVersion(ora: OraImpl) {
async function checkNodeVersion() {
const { engines } = await fs.readJson(path.resolve(__dirname, '..', '..', 'package.json'));
const versionSatisified = semver.satisfies(process.versions.node, engines.node);
const versionSatisfied = semver.satisfies(process.versions.node, engines.node);

if (!versionSatisified) {
ora.warn(`You are running Node.js version ${process.versions.node}, but Electron Forge requires Node.js ${engines.node}.`);
if (!versionSatisfied) {
throw new Error(`You are running Node.js version ${process.versions.node}, but Electron Forge requires Node.js ${engines.node}.`);
}

return versionSatisified;
return process.versions.node;
}

const NPM_WHITELISTED_VERSIONS = {
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_WHITELISTED_VERSIONS = {
const YARN_ALLOWLISTED_VERSIONS = {
all: '0.23.3 || 0.24.6 || >= 1.0.0',
darwin: '0.27.5',
linux: '0.27.5',
};

export function validPackageManagerVersion(packageManager: string, version: string, whitelistedVersions: string, ora: OraImpl): boolean {
try {
return semver.satisfies(version, whitelistedVersions);
} catch (e) {
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
ora.warn!(`Could not check ${packageManager} version "${version}", assuming incompatible`);
d(`Exception while checking version: ${e}`);
return false;
throw new Error(`Could not check ${packageManager} version "${version}", assuming incompatible`);
}
if (!semver.satisfies(version, allowlistedVersions)) {
throw new Error(`Incompatible version of ${packageManager} detected "${version}", must be in range ${allowlistedVersions}`);
}
}

function warnIfPackageManagerIsntAKnownGoodVersion(packageManager: string, version: string, whitelistedVersions: { [key: string]: string }, ora: OraImpl) {
const osVersions = whitelistedVersions[process.platform];
const versions = osVersions ? `${whitelistedVersions.all} || ${osVersions}` : whitelistedVersions.all;
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();
if (!validPackageManagerVersion(packageManager, versionString, versions, ora)) {
ora.warn(`You are using ${packageManager}, but not a known good version.
The known versions that work with Electron Forge are: ${versions}`);
}
checkValidPackageManagerVersion(packageManager, versionString, versions);
}

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

return true;
}

/**
Expand All @@ -82,10 +79,50 @@ async function checkPackageManagerVersion(ora: OraImpl) {
*/
const SKIP_SYSTEM_CHECK = path.resolve(os.homedir(), '.skip-forge-system-check');

export default async function checkSystem(ora: OraImpl): Promise<boolean> {
type SystemCheckContext = {
git: boolean;
node: boolean;
packageManager: boolean;
};
export async function checkSystem(task: ForgeListrTask<never>) {
if (!(await fs.pathExists(SKIP_SYSTEM_CHECK))) {
d('checking system, create ~/.skip-forge-system-check to stop doing this');
return (await Promise.all([checkGitExists(), checkNodeVersion(ora), checkPackageManagerVersion(ora)])).every((check) => check);
return task.newListr<SystemCheckContext>(
[
{
title: 'Checking git exists',
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 node version',
task: async (_, task) => {
const nodeVersion = await checkNodeVersion();
task.title = `Found node@${nodeVersion}`;
},
},
{
title: 'Checking packageManager version',
task: async (_, task) => {
const packageManager = await checkPackageManagerVersion();
task.title = `Found ${packageManager}`;
},
},
],
{
concurrent: true,
exitOnError: false,
rendererOptions: {
collapse: true,
},
}
);
}
d('skipping system check');
return true;
Expand Down
13 changes: 10 additions & 3 deletions packages/api/cli/src/util/terminate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ function redConsoleError(msg: string) {

process.on('unhandledRejection', (reason: string, promise: Promise<unknown>) => {
redConsoleError('\nAn unhandled rejection has occurred inside Forge:');
redConsoleError(reason.toString());
redConsoleError(reason.toString().trim());
MarshallOfSound marked this conversation as resolved.
Show resolved Hide resolved
redConsoleError('\nElectron Forge was terminated. Location:');
redConsoleError(JSON.stringify(promise));
process.exit(1);
promise.catch((err: Error) => {
if ('stack' in err) {
const usefulStack = err.stack;
if (usefulStack?.startsWith(reason.toString().trim())) {
redConsoleError(usefulStack.substring(reason.toString().trim().length + 1).trim());
}
}
process.exit(1);
});
});

process.on('uncaughtException', (err) => {
Expand Down
13 changes: 4 additions & 9 deletions packages/api/cli/test/check-system_spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
import { fakeOra } from '@electron-forge/async-ora';
import { expect } from 'chai';

import checkSystem, { validPackageManagerVersion } from '../src/util/check-system';
import { checkValidPackageManagerVersion } from '../src/util/check-system';

describe('check-system', () => {
it('should succeed on valid agents', async () => {
expect(await checkSystem(fakeOra(''))).to.be.equal(true);
});

describe('validPackageManagerVersion', () => {
it('should consider whitelisted versions to be valid', () => {
expect(validPackageManagerVersion('NPM', '3.10.1', '^3.0.0', fakeOra(''))).to.be.equal(true);
expect(() => checkValidPackageManagerVersion('NPM', '3.10.1', '^3.0.0')).to.not.throw();
});

it('should consider Yarn nightly versions to be invalid', () => {
expect(validPackageManagerVersion('Yarn', '0.23.0-20170311.0515', '0.23.0', fakeOra(''))).to.be.equal(false);
expect(() => checkValidPackageManagerVersion('Yarn', '0.23.0-20170311.0515', '0.23.0')).to.throw();
});

it('should consider invalid semver versions to be invalid', () => {
expect(validPackageManagerVersion('Yarn', '0.22', '0.22.0', fakeOra(''))).to.be.equal(false);
expect(() => checkValidPackageManagerVersion('Yarn', '0.22', '0.22.0')).to.throw();
});
});
});
1 change: 1 addition & 0 deletions packages/api/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"fs-extra": "^10.0.0",
"got": "^11.8.5",
"interpret": "^3.1.1",
"listr2": "^5.0.3",
"lodash": "^4.17.20",
"log-symbols": "^4.0.0",
"node-fetch": "^2.6.7",
Expand Down
2 changes: 1 addition & 1 deletion packages/api/core/src/api/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { info, warn } from '../util/messages';
import { readRawPackageJson } from '../util/read-package-json';
import upgradeForgeConfig, { updateUpgradedForgeDevDeps } from '../util/upgrade-forge-config';

import initGit from './init-scripts/init-git';
import { initGit } from './init-scripts/init-git';
import { deps, devDeps, exactDevDeps } from './init-scripts/init-npm';

const d = debug('electron-forge:import');
Expand Down
53 changes: 25 additions & 28 deletions packages/api/core/src/api/init-scripts/find-template.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { asyncOra } from '@electron-forge/async-ora';
import { ForgeTemplate } from '@electron-forge/shared-types';
import debug from 'debug';
import resolvePackage from 'resolve-package';
Expand All @@ -7,36 +6,34 @@ import { PossibleModule } from '../../util/require-search';

const d = debug('electron-forge:init:find-template');

export default async (dir: string, template: string): Promise<ForgeTemplate> => {
export const findTemplate = async (dir: string, template: string): Promise<ForgeTemplate> => {
let templateModulePath!: string;
await asyncOra(`Locating custom template: "${template}"`, async () => {
const resolveTemplateTypes = [
['global', `electron-forge-template-${template}`],
['global', `@electron-forge/template-${template}`],
['local', `electron-forge-template-${template}`],
['local', `@electron-forge/template-${template}`],
['local', template],
];
let foundTemplate = false;
for (const [templateType, moduleName] of resolveTemplateTypes) {
try {
d(`Trying ${templateType} template: ${moduleName}`);
if (templateType === 'global') {
templateModulePath = await resolvePackage(moduleName);
} else {
// local
templateModulePath = require.resolve(moduleName);
}
foundTemplate = true;
break;
} catch (err) {
d(`Error: ${err instanceof Error ? err.message : err}`);
const resolveTemplateTypes = [
['global', `electron-forge-template-${template}`],
['global', `@electron-forge/template-${template}`],
['local', `electron-forge-template-${template}`],
['local', `@electron-forge/template-${template}`],
['local', template],
];
let foundTemplate = false;
for (const [templateType, moduleName] of resolveTemplateTypes) {
try {
d(`Trying ${templateType} template: ${moduleName}`);
if (templateType === 'global') {
templateModulePath = await resolvePackage(moduleName);
} else {
// local
templateModulePath = require.resolve(moduleName);
}
foundTemplate = true;
break;
} catch (err) {
d(`Error: ${err instanceof Error ? err.message : err}`);
}
if (!foundTemplate) {
throw new Error(`Failed to locate custom template: "${template}"\n\nTry \`npm install -g electron-forge-template-${template}\``);
}
});
}
if (!foundTemplate) {
throw new Error(`Failed to locate custom template: "${template}"\n\nTry \`npm install -g @electron-forge/template-${template}\``);
}

// eslint-disable-next-line @typescript-eslint/no-var-requires
const templateModule: PossibleModule<ForgeTemplate> = require(templateModulePath);
Expand Down
27 changes: 12 additions & 15 deletions packages/api/core/src/api/init-scripts/init-directory.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import { asyncOra } from '@electron-forge/async-ora';
import { ForgeListrTask } from '@electron-forge/shared-types';
import debug from 'debug';
import fs from 'fs-extra';
import logSymbols from 'log-symbols';

const d = debug('electron-forge:init:directory');

export default async (dir: string, force = false): Promise<void> => {
await asyncOra('Initializing Project Directory', async (initSpinner) => {
d('creating directory:', dir);
await fs.mkdirs(dir);
export const initDirectory = async (dir: string, task: ForgeListrTask<any>, force = false): Promise<void> => {
d('creating directory:', dir);
await fs.mkdirs(dir);

const files = await fs.readdir(dir);
if (files.length !== 0) {
d(`found ${files.length} files in the directory. warning the user`);
const files = await fs.readdir(dir);
if (files.length !== 0) {
d(`found ${files.length} files in the directory. warning the user`);

if (force) {
initSpinner.warn(`The specified path "${dir}" is not empty. "force" was set to true, so proceeding to initialize. Files may be overwritten`);
} else {
initSpinner.stop(logSymbols.warning);
throw new Error(`The specified path: "${dir}" is not empty. Please ensure it is empty before initializing a new project`);
}
if (force) {
task.output = `${logSymbols.warning} The specified path "${dir}" is not empty. "force" was set to true, so proceeding to initialize. Files may be overwritten`;
} else {
throw new Error(`The specified path: "${dir}" is not empty. Please ensure it is empty before initializing a new project`);
}
});
}
};
Loading