From c6edca4f1696a383bf15a05d22691d3f62feccd2 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 29 Oct 2022 13:53:29 -0700 Subject: [PATCH 1/9] refactor: port check-system to Listr2 --- package.json | 2 +- packages/api/cli/package.json | 1 + packages/api/cli/src/electron-forge.ts | 26 ++++-- packages/api/cli/src/util/check-system.ts | 103 +++++++++++++++------- packages/utils/types/package.json | 1 + packages/utils/types/src/index.ts | 2 + yarn.lock | 28 +++--- 7 files changed, 108 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 22f76a5f81..12d44d6402 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,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", diff --git a/packages/api/cli/package.json b/packages/api/cli/package.json index f519661c96..94c618b824 100644 --- a/packages/api/cli/package.json +++ b/packages/api/cli/package.json @@ -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": { diff --git a/packages/api/cli/src/electron-forge.ts b/packages/api/cli/src/electron-forge.ts index 9eb258453a..1710958302 100755 --- a/packages/api/cli/src/electron-forge.ts +++ b/packages/api/cli/src/electron-forge.ts @@ -1,9 +1,9 @@ #!/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'; @@ -50,14 +50,26 @@ program }); (async () => { - let goodSystem; - await asyncOra('Checking your system', async (ora) => { - goodSystem = await checkSystem(ora); - }); + const runner = new Listr( + [ + { + 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); diff --git a/packages/api/cli/src/util/check-system.ts b/packages/api/cli/src/util/check-system.ts index d117f68a47..4dfedb217c 100644 --- a/packages/api/cli/src/util/check-system.ts +++ b/packages/api/cli/src/util/check-system.ts @@ -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((resolve) => { - exec('git --version', (err) => resolve(!err)); +async function getGitVersion(): Promise { + return new Promise((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; } /** @@ -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 { +type SystemCheckContext = { + git: boolean; + node: boolean; + packageManager: boolean; +}; +export default async function checkSystem(task: ForgeListrTask) { 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( + [ + { + 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; diff --git a/packages/utils/types/package.json b/packages/utils/types/package.json index 9aa6cf5416..29c5a40c35 100644 --- a/packages/utils/types/package.json +++ b/packages/utils/types/package.json @@ -11,6 +11,7 @@ "@electron-forge/async-ora": "6.0.0-beta.70", "electron-packager": "^17.0.0", "electron-rebuild": "^3.2.6", + "listr2": "^5.0.3", "ora": "^5.0.0" }, "engines": { diff --git a/packages/utils/types/src/index.ts b/packages/utils/types/src/index.ts index fda98ebb26..dabb57573d 100644 --- a/packages/utils/types/src/index.ts +++ b/packages/utils/types/src/index.ts @@ -3,7 +3,9 @@ import { ChildProcess } from 'child_process'; import { OraImpl } from '@electron-forge/async-ora'; import { ArchOption, Options as ElectronPackagerOptions, TargetPlatform } from 'electron-packager'; import { RebuildOptions } from 'electron-rebuild'; +import { ListrDefaultRenderer, ListrTaskWrapper } from 'listr2'; +export type ForgeListrTask = ListrTaskWrapper; export type ElectronProcess = ChildProcess & { restarted: boolean }; export type ForgePlatform = TargetPlatform; diff --git a/yarn.lock b/yarn.lock index d193a3c293..625998d1c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3253,7 +3253,7 @@ color-support@^1.1.2: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -colorette@^2.0.10, colorette@^2.0.17: +colorette@^2.0.10, colorette@^2.0.17, colorette@^2.0.19: version "2.0.19" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== @@ -6310,31 +6310,31 @@ lint-staged@^13.0.3: string-argv "^0.3.1" yaml "^2.1.1" -listr2@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-4.0.4.tgz#d098a1c419284fb26e184b5d5889b235e8912245" - integrity sha512-vJOm5KD6uZXjSsrwajr+mNacIjf87gWvlBEltPWLbTkslUscWAzquyK4xfe9Zd4RDgO5nnwFyV06FC+uVR+5mg== +listr2@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-4.0.5.tgz#9dcc50221583e8b4c71c43f9c7dfd0ef546b75d5" + integrity sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA== dependencies: cli-truncate "^2.1.0" colorette "^2.0.16" log-update "^4.0.0" p-map "^4.0.0" rfdc "^1.3.0" - rxjs "^7.5.4" + rxjs "^7.5.5" through "^2.3.8" wrap-ansi "^7.0.0" -listr2@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-4.0.5.tgz#9dcc50221583e8b4c71c43f9c7dfd0ef546b75d5" - integrity sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA== +listr2@^5.0.3: + version "5.0.5" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-5.0.5.tgz#4651a940d12b984abecfae4450e40edd5695f808" + integrity sha512-DpBel6fczu7oQKTXMekeprc0o3XDgGMkD7JNYyX+X0xbwK+xgrx9dcyKoXKqpLSUvAWfmoePS7kavniOcq3r4w== dependencies: cli-truncate "^2.1.0" - colorette "^2.0.16" + colorette "^2.0.19" log-update "^4.0.0" p-map "^4.0.0" rfdc "^1.3.0" - rxjs "^7.5.5" + rxjs "^7.5.6" through "^2.3.8" wrap-ansi "^7.0.0" @@ -8119,14 +8119,14 @@ rxjs@^6.4.0, rxjs@^6.5.1: dependencies: tslib "^1.9.0" -rxjs@^7.2.0, rxjs@^7.5.4: +rxjs@^7.2.0: version "7.5.4" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.4.tgz#3d6bd407e6b7ce9a123e76b1e770dc5761aa368d" integrity sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ== dependencies: tslib "^2.1.0" -rxjs@^7.5.5: +rxjs@^7.5.5, rxjs@^7.5.6: version "7.5.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== From 4dab19786c446f9661c4a42de6a25bb6b4e1b555 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 29 Oct 2022 23:35:29 -0700 Subject: [PATCH 2/9] refactor: port 'init' to listr2 --- packages/api/cli/src/electron-forge.ts | 2 +- packages/api/cli/src/util/check-system.ts | 2 +- packages/api/cli/test/check-system_spec.ts | 13 +- packages/api/core/package.json | 1 + packages/api/core/src/api/import.ts | 2 +- .../src/api/init-scripts/find-template.ts | 53 ++++---- .../src/api/init-scripts/init-directory.ts | 27 ++-- .../api/core/src/api/init-scripts/init-git.ts | 37 +++--- .../api/core/src/api/init-scripts/init-npm.ts | 33 ++--- packages/api/core/src/api/init.ts | 116 ++++++++++++++---- .../api/core/test/slow/init_git_spec_slow.ts | 2 +- packages/template/base/src/BaseTemplate.ts | 60 +++++---- .../template/typescript-webpack/package.json | 3 +- .../src/TypeScriptWebpackTemplate.ts | 104 ++++++++-------- .../test/TypeScriptWebpackTemplate_spec.ts | 6 +- packages/template/webpack/package.json | 3 +- .../template/webpack/src/WebpackTemplate.ts | 76 +++++++----- .../webpack/test/WebpackTemplate_spec.ts | 6 +- packages/utils/types/src/index.ts | 6 +- 19 files changed, 323 insertions(+), 229 deletions(-) diff --git a/packages/api/cli/src/electron-forge.ts b/packages/api/cli/src/electron-forge.ts index 1710958302..b4730fb8c5 100755 --- a/packages/api/cli/src/electron-forge.ts +++ b/packages/api/cli/src/electron-forge.ts @@ -7,7 +7,7 @@ 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'); diff --git a/packages/api/cli/src/util/check-system.ts b/packages/api/cli/src/util/check-system.ts index 4dfedb217c..c64f8c4b70 100644 --- a/packages/api/cli/src/util/check-system.ts +++ b/packages/api/cli/src/util/check-system.ts @@ -84,7 +84,7 @@ type SystemCheckContext = { node: boolean; packageManager: boolean; }; -export default async function checkSystem(task: ForgeListrTask) { +export async function checkSystem(task: ForgeListrTask) { if (!(await fs.pathExists(SKIP_SYSTEM_CHECK))) { d('checking system, create ~/.skip-forge-system-check to stop doing this'); return task.newListr( diff --git a/packages/api/cli/test/check-system_spec.ts b/packages/api/cli/test/check-system_spec.ts index df5f499180..0b47f0dc17 100644 --- a/packages/api/cli/test/check-system_spec.ts +++ b/packages/api/cli/test/check-system_spec.ts @@ -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(); }); }); }); diff --git a/packages/api/core/package.json b/packages/api/core/package.json index 0c72c2edc3..2e4080c534 100644 --- a/packages/api/core/package.json +++ b/packages/api/core/package.json @@ -59,6 +59,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", diff --git a/packages/api/core/src/api/import.ts b/packages/api/core/src/api/import.ts index 981788f249..b2591e3f20 100644 --- a/packages/api/core/src/api/import.ts +++ b/packages/api/core/src/api/import.ts @@ -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'); diff --git a/packages/api/core/src/api/init-scripts/find-template.ts b/packages/api/core/src/api/init-scripts/find-template.ts index d9973694c2..f256b38230 100644 --- a/packages/api/core/src/api/init-scripts/find-template.ts +++ b/packages/api/core/src/api/init-scripts/find-template.ts @@ -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'; @@ -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 => { +export const findTemplate = async (dir: string, template: string): Promise => { 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 = require(templateModulePath); diff --git a/packages/api/core/src/api/init-scripts/init-directory.ts b/packages/api/core/src/api/init-scripts/init-directory.ts index c7a8a85779..5af2f8b461 100644 --- a/packages/api/core/src/api/init-scripts/init-directory.ts +++ b/packages/api/core/src/api/init-scripts/init-directory.ts @@ -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 => { - await asyncOra('Initializing Project Directory', async (initSpinner) => { - d('creating directory:', dir); - await fs.mkdirs(dir); +export const initDirectory = async (dir: string, task: ForgeListrTask, force = false): Promise => { + 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`); } - }); + } }; diff --git a/packages/api/core/src/api/init-scripts/init-git.ts b/packages/api/core/src/api/init-scripts/init-git.ts index c18163c1ee..4285b3691d 100644 --- a/packages/api/core/src/api/init-scripts/init-git.ts +++ b/packages/api/core/src/api/init-scripts/init-git.ts @@ -1,29 +1,26 @@ import { exec } from 'child_process'; -import { asyncOra } from '@electron-forge/async-ora'; import debug from 'debug'; const d = debug('electron-forge:init:git'); -export default async (dir: string): Promise => { - await asyncOra('Initializing Git repository', async () => { - await new Promise((resolve, reject) => { - exec( - 'git rev-parse --show-toplevel', - { - cwd: dir, - }, - (err) => { - if (err) { - // not run within a Git repository - d('executing "git init" in directory:', dir); - exec('git init', { cwd: dir }, (initErr) => (initErr ? reject(initErr) : resolve())); - } else { - d('.git directory already exists, skipping git initialization'); - resolve(); - } +export const initGit = async (dir: string): Promise => { + await new Promise((resolve, reject) => { + exec( + 'git rev-parse --show-toplevel', + { + cwd: dir, + }, + (err) => { + if (err) { + // not run within a Git repository + d('executing "git init" in directory:', dir); + exec('git init', { cwd: dir }, (initErr) => (initErr ? reject(initErr) : resolve())); + } else { + d('.git directory already exists, skipping git initialization'); + resolve(); } - ); - }); + } + ); }); }; 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 4a4e221b4d..daec1920e8 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -1,10 +1,11 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; +import { ForgeListrTask } from '@electron-forge/shared-types'; import debug from 'debug'; import fs from 'fs-extra'; import installDepList, { DepType, DepVersionRestriction } from '../../util/install-dependencies'; +import safeYarnOrNpm from '../../util/yarn-or-npm'; const d = debug('electron-forge:init:npm'); const corePackage = fs.readJsonSync(path.resolve(__dirname, '../../../package.json')); @@ -17,17 +18,21 @@ export const deps = ['electron-squirrel-startup']; export const devDeps = [siblingDep('cli'), siblingDep('maker-squirrel'), siblingDep('maker-zip'), siblingDep('maker-deb'), siblingDep('maker-rpm')]; export const exactDevDeps = ['electron']; -export default async (dir: string): Promise => { - await asyncOra('Installing NPM Dependencies', async () => { - d('installing dependencies'); - await installDepList(dir, deps); - - d('installing devDependencies'); - await installDepList(dir, devDeps, DepType.DEV); - - d('installing exact devDependencies'); - for (const packageName of exactDevDeps) { - await installDepList(dir, [packageName], DepType.DEV, DepVersionRestriction.EXACT); - } - }); +export const initNPM = async (dir: string, task: ForgeListrTask): Promise => { + // await asyncOra('Installing NPM Dependencies', async () => { + d('installing dependencies'); + const packageManager = safeYarnOrNpm(); + task.output = `${packageManager} install ${deps.join(' ')}`; + await installDepList(dir, deps); + + d('installing devDependencies'); + task.output = `${packageManager} install --dev ${deps.join(' ')}`; + await installDepList(dir, devDeps, DepType.DEV); + + d('installing exact devDependencies'); + for (const packageName of exactDevDeps) { + task.output = `${packageManager} install --dev --exact ${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 e132e5a090..467bb4b2c6 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -1,17 +1,18 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; import { ForgeTemplate } from '@electron-forge/shared-types'; import debug from 'debug'; +import { Listr } from 'listr2'; import semver from 'semver'; -import installDepList, { DepType } from '../util/install-dependencies'; +import installDepList, { DepType, DepVersionRestriction } from '../util/install-dependencies'; import { readRawPackageJson } from '../util/read-package-json'; +import safeYarnOrNpm from '../util/yarn-or-npm'; -import findTemplate from './init-scripts/find-template'; -import initDirectory from './init-scripts/init-directory'; -import initGit from './init-scripts/init-git'; -import initNPM from './init-scripts/init-npm'; +import { findTemplate } from './init-scripts/find-template'; +import { initDirectory } from './init-scripts/init-directory'; +import { initGit } from './init-scripts/init-git'; +import { initNPM } from './init-scripts/init-npm'; const d = debug('electron-forge:init'); @@ -52,26 +53,93 @@ async function validateTemplate(template: string, templateModule: ForgeTemplate) } export default async ({ dir = process.cwd(), interactive = false, copyCIFiles = false, force = false, template = 'base' }: InitOptions): Promise => { - asyncOra.interactive = interactive; - d(`Initializing in: ${dir}`); - await initDirectory(dir, force); - await initGit(dir); - const templateModule = await findTemplate(dir, template); - - await validateTemplate(template, templateModule); - - if (typeof templateModule.initializeTemplate === 'function') { - await templateModule.initializeTemplate(dir, { copyCIFiles }); - } + const packageManager = safeYarnOrNpm(); - await asyncOra('Installing Template Dependencies', async () => { - d('installing dependencies'); - await installDepList(dir, templateModule.dependencies || []); - d('installing devDependencies'); - await installDepList(dir, templateModule.devDependencies || [], DepType.DEV); - }); + const runner = new Listr<{ + templateModule: ForgeTemplate; + }>( + [ + { + title: `Locating custom template: "${template}"`, + task: async (ctx) => { + ctx.templateModule = await findTemplate(dir, template); + }, + }, + { + title: 'Initializing directory', + task: async (_, task) => { + await initDirectory(dir, task, force); + await initGit(dir); + }, + options: { + persistentOutput: true, + }, + }, + { + title: 'Preparing template', + task: async ({ templateModule }) => { + await validateTemplate(template, templateModule); + }, + }, + { + title: 'Initializing template', + task: async ({ templateModule }, task) => { + if (typeof templateModule.initializeTemplate === 'function') { + const tasks = await templateModule.initializeTemplate(dir, { copyCIFiles }); + if (tasks) { + return task.newListr(tasks, { concurrent: false }); + } + } + }, + }, + { + title: 'Installing template dependencies', + task: async ({ templateModule }, task) => { + return task.newListr( + [ + { + title: 'Installing production dependencies', + task: async (_, task) => { + d('installing dependencies'); + if (templateModule.dependencies?.length) { + task.output = `${packageManager} install ${templateModule.dependencies.join(' ')}`; + } + return await installDepList(dir, templateModule.dependencies || [], DepType.PROD, DepVersionRestriction.RANGE); + }, + }, + { + title: 'Installing development dependencies', + task: async (_, task) => { + d('installing devDependencies'); + if (templateModule.devDependencies?.length) { + task.output = `${packageManager} install --dev ${templateModule.devDependencies.join(' ')}`; + } + await installDepList(dir, templateModule.devDependencies || [], DepType.DEV); + }, + }, + { + title: 'Finalizing dependencies', + task: async (_, task) => { + await initNPM(dir, task); + }, + }, + ], + { + concurrent: false, + exitOnError: false, + } + ); + }, + }, + ], + { + concurrent: false, + rendererSilent: !interactive, + rendererFallback: Boolean(process.env.DEBUG && process.env.DEBUG.includes('electron-forge')), + } + ); - await initNPM(dir); + await runner.run(); }; diff --git a/packages/api/core/test/slow/init_git_spec_slow.ts b/packages/api/core/test/slow/init_git_spec_slow.ts index 0b57a9ea00..c29e1938f7 100644 --- a/packages/api/core/test/slow/init_git_spec_slow.ts +++ b/packages/api/core/test/slow/init_git_spec_slow.ts @@ -5,7 +5,7 @@ import path from 'path'; import { expect } from 'chai'; import fs from 'fs-extra'; -import initGit from '../../src/api/init-scripts/init-git'; +import { initGit } from '../../src/api/init-scripts/init-git'; let dir: string; let dirID = Date.now(); diff --git a/packages/template/base/src/BaseTemplate.ts b/packages/template/base/src/BaseTemplate.ts index 15e6ac59cc..0d66cc78c1 100644 --- a/packages/template/base/src/BaseTemplate.ts +++ b/packages/template/base/src/BaseTemplate.ts @@ -1,7 +1,6 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; -import { ForgeTemplate, InitTemplateOptions } from '@electron-forge/shared-types'; +import { ForgeListrTaskDefinition, ForgeTemplate, InitTemplateOptions } from '@electron-forge/shared-types'; import debug from 'debug'; import fs from 'fs-extra'; @@ -35,23 +34,32 @@ export class BaseTemplate implements ForgeTemplate { return []; } - public async initializeTemplate(directory: string, { copyCIFiles }: InitTemplateOptions): Promise { - await asyncOra('Copying Starter Files', async () => { - d('creating directory:', path.resolve(directory, 'src')); - await fs.mkdirs(path.resolve(directory, 'src')); - const rootFiles = ['_gitignore', 'forge.config.js']; - if (copyCIFiles) rootFiles.push(...['_travis.yml', '_appveyor.yml']); - const srcFiles = ['index.css', 'index.js', 'index.html', 'preload.js']; - - for (const file of rootFiles) { - await this.copy(path.resolve(tmplDir, file), path.resolve(directory, file.replace(/^_/, '.'))); - } - for (const file of srcFiles) { - await this.copy(path.resolve(tmplDir, file), path.resolve(directory, 'src', file)); - } - }); - - await this.initializePackageJSON(directory); + public async initializeTemplate(directory: string, { copyCIFiles }: InitTemplateOptions): Promise { + return [ + { + title: 'Copying starter files', + task: async () => { + d('creating directory:', path.resolve(directory, 'src')); + await fs.mkdirs(path.resolve(directory, 'src')); + const rootFiles = ['_gitignore', 'forge.config.js']; + if (copyCIFiles) rootFiles.push(...['_travis.yml', '_appveyor.yml']); + const srcFiles = ['index.css', 'index.js', 'index.html', 'preload.js']; + + for (const file of rootFiles) { + await this.copy(path.resolve(tmplDir, file), path.resolve(directory, file.replace(/^_/, '.'))); + } + for (const file of srcFiles) { + await this.copy(path.resolve(tmplDir, file), path.resolve(directory, 'src', file)); + } + }, + }, + { + title: 'Initializing package.json', + task: async () => { + await this.initializePackageJSON(directory); + }, + }, + ]; } async copy(source: string, target: string): Promise { @@ -65,16 +73,14 @@ export class BaseTemplate implements ForgeTemplate { } async initializePackageJSON(directory: string): Promise { - await asyncOra('Initializing NPM Module', async () => { - const packageJSON = await fs.readJson(path.resolve(__dirname, '../tmpl/package.json')); - packageJSON.productName = packageJSON.name = path.basename(directory).toLowerCase(); - packageJSON.author = await determineAuthor(directory); + const packageJSON = await fs.readJson(path.resolve(__dirname, '../tmpl/package.json')); + packageJSON.productName = packageJSON.name = path.basename(directory).toLowerCase(); + packageJSON.author = await determineAuthor(directory); - packageJSON.scripts.lint = 'echo "No linting configured"'; + packageJSON.scripts.lint = 'echo "No linting configured"'; - d('writing package.json to:', directory); - await fs.writeJson(path.resolve(directory, 'package.json'), packageJSON, { spaces: 2 }); - }); + d('writing package.json to:', directory); + await fs.writeJson(path.resolve(directory, 'package.json'), packageJSON, { spaces: 2 }); } async updateFileByLine(inputPath: string, lineHandler: (line: string) => string, outputPath?: string | undefined): Promise { diff --git a/packages/template/typescript-webpack/package.json b/packages/template/typescript-webpack/package.json index 228e35085d..e91a92351a 100644 --- a/packages/template/typescript-webpack/package.json +++ b/packages/template/typescript-webpack/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@electron-forge/test-utils": "6.0.0-beta.70", "chai": "^4.3.3", - "fast-glob": "^3.2.7" + "fast-glob": "^3.2.7", + "listr2": "^5.0.3" } } diff --git a/packages/template/typescript-webpack/src/TypeScriptWebpackTemplate.ts b/packages/template/typescript-webpack/src/TypeScriptWebpackTemplate.ts index 250f93d5f1..ff5a2efa34 100644 --- a/packages/template/typescript-webpack/src/TypeScriptWebpackTemplate.ts +++ b/packages/template/typescript-webpack/src/TypeScriptWebpackTemplate.ts @@ -1,59 +1,67 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; -import { InitTemplateOptions } from '@electron-forge/shared-types'; +import { ForgeListrTaskDefinition, InitTemplateOptions } from '@electron-forge/shared-types'; import { BaseTemplate } from '@electron-forge/template-base'; import fs from 'fs-extra'; class TypeScriptWebpackTemplate extends BaseTemplate { public templateDir = path.resolve(__dirname, '..', 'tmpl'); - async initializeTemplate(directory: string, options: InitTemplateOptions) { - await super.initializeTemplate(directory, options); - await asyncOra('Setting up Forge configuration', async () => { - await this.copyTemplateFile(directory, 'forge.config.ts'); - await fs.remove(path.resolve(directory, 'forge.config.js')); - }); - await asyncOra('Setting up TypeScript configuration', async () => { - const filePath = (fileName: string) => path.join(directory, 'src', fileName); - - // Copy Webpack files - await this.copyTemplateFile(directory, 'webpack.main.config.js'); - await this.copyTemplateFile(directory, 'webpack.renderer.config.js'); - await this.copyTemplateFile(directory, 'webpack.rules.js'); - await this.copyTemplateFile(directory, 'webpack.plugins.js'); - - await this.updateFileByLine(path.resolve(directory, 'src', 'index.html'), (line) => { - if (line.includes('link rel="stylesheet"')) return ''; - return line; - }); - - // Copy tsconfig with a small set of presets - await this.copyTemplateFile(directory, 'tsconfig.json'); - - // Copy eslint config with recommended settings - await this.copyTemplateFile(directory, '.eslintrc.json'); - - // Remove index.js and replace with index.ts - await fs.remove(filePath('index.js')); - await this.copyTemplateFile(path.join(directory, 'src'), 'index.ts'); - - await this.copyTemplateFile(path.join(directory, 'src'), 'renderer.ts'); - - // Remove preload.js and replace with preload.ts - await fs.remove(filePath('preload.js')); - await this.copyTemplateFile(path.join(directory, 'src'), 'preload.ts'); - - // update package.json - const packageJSONPath = path.resolve(directory, 'package.json'); - const packageJSON = await fs.readJson(packageJSONPath); - packageJSON.main = '.webpack/main'; - // Configure scripts for TS template - packageJSON.scripts.lint = 'eslint --ext .ts,.tsx .'; - await fs.writeJson(packageJSONPath, packageJSON, { - spaces: 2, - }); - }); + async initializeTemplate(directory: string, options: InitTemplateOptions): Promise { + const superTasks = await super.initializeTemplate(directory, options); + return [ + ...superTasks, + { + title: 'Setting up Forge configuration', + task: async () => { + await this.copyTemplateFile(directory, 'forge.config.ts'); + await fs.remove(path.resolve(directory, 'forge.config.js')); + }, + }, + { + title: 'Preparing TypeScript files and configuration', + task: async () => { + const filePath = (fileName: string) => path.join(directory, 'src', fileName); + + // Copy Webpack files + await this.copyTemplateFile(directory, 'webpack.main.config.js'); + await this.copyTemplateFile(directory, 'webpack.renderer.config.js'); + await this.copyTemplateFile(directory, 'webpack.rules.js'); + await this.copyTemplateFile(directory, 'webpack.plugins.js'); + + await this.updateFileByLine(path.resolve(directory, 'src', 'index.html'), (line) => { + if (line.includes('link rel="stylesheet"')) return ''; + return line; + }); + + // Copy tsconfig with a small set of presets + await this.copyTemplateFile(directory, 'tsconfig.json'); + + // Copy eslint config with recommended settings + await this.copyTemplateFile(directory, '.eslintrc.json'); + + // Remove index.js and replace with index.ts + await fs.remove(filePath('index.js')); + await this.copyTemplateFile(path.join(directory, 'src'), 'index.ts'); + + await this.copyTemplateFile(path.join(directory, 'src'), 'renderer.ts'); + + // Remove preload.js and replace with preload.ts + await fs.remove(filePath('preload.js')); + await this.copyTemplateFile(path.join(directory, 'src'), 'preload.ts'); + + // update package.json + const packageJSONPath = path.resolve(directory, 'package.json'); + const packageJSON = await fs.readJson(packageJSONPath); + packageJSON.main = '.webpack/main'; + // Configure scripts for TS template + packageJSON.scripts.lint = 'eslint --ext .ts,.tsx .'; + await fs.writeJson(packageJSONPath, packageJSON, { + spaces: 2, + }); + }, + }, + ]; } } diff --git a/packages/template/typescript-webpack/test/TypeScriptWebpackTemplate_spec.ts b/packages/template/typescript-webpack/test/TypeScriptWebpackTemplate_spec.ts index 3692bfb927..443532f6d7 100644 --- a/packages/template/typescript-webpack/test/TypeScriptWebpackTemplate_spec.ts +++ b/packages/template/typescript-webpack/test/TypeScriptWebpackTemplate_spec.ts @@ -4,6 +4,7 @@ import * as testUtils from '@electron-forge/test-utils'; import { expect } from 'chai'; import glob from 'fast-glob'; import fs from 'fs-extra'; +import { Listr } from 'listr2'; import template from '../src/TypeScriptWebpackTemplate'; @@ -15,7 +16,10 @@ describe('TypeScriptWebpackTemplate', () => { }); it('should succeed in initializing the typescript template', async () => { - await template.initializeTemplate(dir, {}); + const tasks = await template.initializeTemplate(dir, {}); + const runner = new Listr(tasks, { concurrent: false, exitOnError: false }); + await runner.run(); + expect(runner.err).to.have.lengthOf(0); }); context('template files are copied to project', () => { diff --git a/packages/template/webpack/package.json b/packages/template/webpack/package.json index 13bb25a462..06c005443d 100644 --- a/packages/template/webpack/package.json +++ b/packages/template/webpack/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@electron-forge/test-utils": "6.0.0-beta.70", - "chai": "^4.3.3" + "chai": "^4.3.3", + "listr2": "^5.0.3" } } diff --git a/packages/template/webpack/src/WebpackTemplate.ts b/packages/template/webpack/src/WebpackTemplate.ts index 8136b5b243..bde2bb63c5 100644 --- a/packages/template/webpack/src/WebpackTemplate.ts +++ b/packages/template/webpack/src/WebpackTemplate.ts @@ -1,48 +1,56 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; -import { InitTemplateOptions } from '@electron-forge/shared-types'; +import { ForgeListrTaskDefinition, InitTemplateOptions } from '@electron-forge/shared-types'; import { BaseTemplate } from '@electron-forge/template-base'; import fs from 'fs-extra'; class WebpackTemplate extends BaseTemplate { public templateDir = path.resolve(__dirname, '..', 'tmpl'); - public async initializeTemplate(directory: string, options: InitTemplateOptions) { - await super.initializeTemplate(directory, options); - await asyncOra('Setting up Forge configuration', async () => { - await this.copyTemplateFile(directory, 'forge.config.js'); - }); - await asyncOra('Setting up webpack configuration', async () => { - await this.copyTemplateFile(directory, 'webpack.main.config.js'); - await this.copyTemplateFile(directory, 'webpack.renderer.config.js'); - await this.copyTemplateFile(directory, 'webpack.rules.js'); - await this.copyTemplateFile(path.join(directory, 'src'), 'renderer.js'); - await this.copyTemplateFile(path.join(directory, 'src'), 'preload.js'); - - await this.updateFileByLine( - path.resolve(directory, 'src', 'index.js'), - (line) => { - if (line.includes('mainWindow.loadFile')) return ' mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);'; - if (line.includes('preload: ')) return ' preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,'; - return line; + public async initializeTemplate(directory: string, options: InitTemplateOptions): Promise { + const superTasks = await super.initializeTemplate(directory, options); + return [ + ...superTasks, + { + title: 'Setting up Forge configuration', + task: async () => { + await this.copyTemplateFile(directory, 'forge.config.js'); }, - path.resolve(directory, 'src', 'main.js') - ); + }, + { + title: 'Setting up webpack configuration', + task: async () => { + await this.copyTemplateFile(directory, 'webpack.main.config.js'); + await this.copyTemplateFile(directory, 'webpack.renderer.config.js'); + await this.copyTemplateFile(directory, 'webpack.rules.js'); + await this.copyTemplateFile(path.join(directory, 'src'), 'renderer.js'); + await this.copyTemplateFile(path.join(directory, 'src'), 'preload.js'); + + await this.updateFileByLine( + path.resolve(directory, 'src', 'index.js'), + (line) => { + if (line.includes('mainWindow.loadFile')) return ' mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);'; + if (line.includes('preload: ')) return ' preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,'; + return line; + }, + path.resolve(directory, 'src', 'main.js') + ); - await this.updateFileByLine(path.resolve(directory, 'src', 'index.html'), (line) => { - if (line.includes('link rel="stylesheet"')) return ''; - return line; - }); + await this.updateFileByLine(path.resolve(directory, 'src', 'index.html'), (line) => { + if (line.includes('link rel="stylesheet"')) return ''; + return line; + }); - // update package.json entry point - const pjPath = path.resolve(directory, 'package.json'); - const currentPJ = await fs.readJson(pjPath); - currentPJ.main = '.webpack/main'; - await fs.writeJson(pjPath, currentPJ, { - spaces: 2, - }); - }); + // update package.json entry point + const pjPath = path.resolve(directory, 'package.json'); + const currentPJ = await fs.readJson(pjPath); + currentPJ.main = '.webpack/main'; + await fs.writeJson(pjPath, currentPJ, { + spaces: 2, + }); + }, + }, + ]; } } diff --git a/packages/template/webpack/test/WebpackTemplate_spec.ts b/packages/template/webpack/test/WebpackTemplate_spec.ts index 6cbad502f4..256f02f18d 100644 --- a/packages/template/webpack/test/WebpackTemplate_spec.ts +++ b/packages/template/webpack/test/WebpackTemplate_spec.ts @@ -3,6 +3,7 @@ import path from 'path'; import * as testUtils from '@electron-forge/test-utils'; import { expect } from 'chai'; import fs from 'fs-extra'; +import { Listr } from 'listr2'; import template from '../src/WebpackTemplate'; @@ -14,7 +15,10 @@ describe('WebpackTemplate', () => { }); it('should succeed in initializing the webpack template', async () => { - await template.initializeTemplate(dir, {}); + const tasks = await template.initializeTemplate(dir, {}); + const runner = new Listr(tasks, { concurrent: false, exitOnError: false }); + await runner.run(); + expect(runner.err).to.have.lengthOf(0); }); context('template files are copied to project', () => { diff --git a/packages/utils/types/src/index.ts b/packages/utils/types/src/index.ts index dabb57573d..a44efdca52 100644 --- a/packages/utils/types/src/index.ts +++ b/packages/utils/types/src/index.ts @@ -3,7 +3,7 @@ import { ChildProcess } from 'child_process'; import { OraImpl } from '@electron-forge/async-ora'; import { ArchOption, Options as ElectronPackagerOptions, TargetPlatform } from 'electron-packager'; import { RebuildOptions } from 'electron-rebuild'; -import { ListrDefaultRenderer, ListrTaskWrapper } from 'listr2'; +import { ListrDefaultRenderer, ListrTask, ListrTaskWrapper } from 'listr2'; export type ForgeListrTask = ListrTaskWrapper; export type ElectronProcess = ChildProcess & { restarted: boolean }; @@ -193,11 +193,13 @@ export interface InitTemplateOptions { copyCIFiles?: boolean; } +export type ForgeListrTaskDefinition = ListrTask; + export interface ForgeTemplate { requiredForgeVersion?: string; dependencies?: string[]; devDependencies?: string[]; - initializeTemplate?: (dir: string, options: InitTemplateOptions) => Promise; + initializeTemplate?: (dir: string, options: InitTemplateOptions) => Promise; } export type PackagePerson = From 9e226490292d85c2b2e542ce543e5ee9f61b7b8a Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 30 Oct 2022 15:36:52 -0700 Subject: [PATCH 3/9] refactor: port 'start' to listr2 --- .../src/api/init-scripts/init-directory.ts | 2 +- .../api/core/src/api/init-scripts/init-npm.ts | 4 +- packages/api/core/src/api/start.ts | 134 +++++++++---- .../api/core/src/util/plugin-interface.ts | 12 +- packages/api/core/src/util/rebuild.ts | 76 +++++++- packages/api/core/src/util/remote-rebuild.ts | 32 ++++ packages/api/core/src/util/require-search.ts | 11 ++ packages/plugin/base/src/Plugin.ts | 4 +- packages/plugin/webpack/src/WebpackPlugin.ts | 181 ++++++++++-------- .../webpack/src/util/ElectronForgeLogging.ts | 43 +---- .../typescript-webpack/tmpl/tsconfig.json | 1 + packages/utils/types/src/index.ts | 5 +- yarn.lock | 6 +- 13 files changed, 334 insertions(+), 177 deletions(-) create mode 100644 packages/api/core/src/util/remote-rebuild.ts diff --git a/packages/api/core/src/api/init-scripts/init-directory.ts b/packages/api/core/src/api/init-scripts/init-directory.ts index 5af2f8b461..cc35bddf93 100644 --- a/packages/api/core/src/api/init-scripts/init-directory.ts +++ b/packages/api/core/src/api/init-scripts/init-directory.ts @@ -5,7 +5,7 @@ import logSymbols from 'log-symbols'; const d = debug('electron-forge:init:directory'); -export const initDirectory = async (dir: string, task: ForgeListrTask, force = false): Promise => { +export const initDirectory = async (dir: string, task: ForgeListrTask, force = false): Promise => { d('creating directory:', dir); await fs.mkdirs(dir); 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 daec1920e8..135b309aaa 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -18,8 +18,7 @@ export const deps = ['electron-squirrel-startup']; export const devDeps = [siblingDep('cli'), siblingDep('maker-squirrel'), siblingDep('maker-zip'), siblingDep('maker-deb'), siblingDep('maker-rpm')]; export const exactDevDeps = ['electron']; -export const initNPM = async (dir: string, task: ForgeListrTask): Promise => { - // await asyncOra('Installing NPM Dependencies', async () => { +export const initNPM = async (dir: string, task: ForgeListrTask): Promise => { d('installing dependencies'); const packageManager = safeYarnOrNpm(); task.output = `${packageManager} install ${deps.join(' ')}`; @@ -34,5 +33,4 @@ export const initNPM = async (dir: string, task: ForgeListrTask): Promi task.output = `${packageManager} install --dev --exact ${packageName}`; await installDepList(dir, [packageName], DepType.DEV, DepVersionRestriction.EXACT); } - // }); }; diff --git a/packages/api/core/src/api/start.ts b/packages/api/core/src/api/start.ts index 3884f8494c..3acb16b03f 100644 --- a/packages/api/core/src/api/start.ts +++ b/packages/api/core/src/api/start.ts @@ -1,24 +1,31 @@ import { spawn, SpawnOptions } from 'child_process'; -import { asyncOra } from '@electron-forge/async-ora'; -import { ElectronProcess, ForgeArch, ForgePlatform, StartOptions } from '@electron-forge/shared-types'; +import { ElectronProcess, ForgeArch, ForgePlatform, ResolvedForgeConfig, StartOptions } from '@electron-forge/shared-types'; import chalk from 'chalk'; import debug from 'debug'; +import { Listr } from 'listr2'; import locateElectronExecutable from '../util/electron-executable'; import { getElectronVersion } from '../util/electron-version'; import getForgeConfig from '../util/forge-config'; import { runHook } from '../util/hook'; import { readMutatedPackageJson } from '../util/read-package-json'; -import rebuild from '../util/rebuild'; +import { listrCompatibleRebuildHook } from '../util/rebuild'; import resolveDir from '../util/resolve-dir'; const d = debug('electron-forge:start'); export { StartOptions }; +type StartContext = { + dir: string; + forgeConfig: ResolvedForgeConfig; + packageJSON: any; + spawned: ElectronProcess; +}; + export default async ({ - dir = process.cwd(), + dir: providedDir = process.cwd(), appPath = '.', interactive = false, enableLogging = false, @@ -27,42 +34,79 @@ export default async ({ inspect = false, inspectBrk = false, }: StartOptions): Promise => { - asyncOra.interactive = interactive; - // Since the `start` command is meant to be long-living (i.e. run forever, - // until interrupted) we should enable this to keep stdin flowing after ora - // completes. For more context: - // https://github.com/electron/forge/issues/2319 - asyncOra.keepStdinFlowing = true; - - await asyncOra('Locating Application', async () => { - const resolvedDir = await resolveDir(dir); - if (!resolvedDir) { - throw new Error('Failed to locate startable Electron application'); - } - dir = resolvedDir; - }); - - const forgeConfig = await getForgeConfig(dir); - const packageJSON = await readMutatedPackageJson(dir, forgeConfig); - - if (!packageJSON.version) { - throw new Error(`Please set your application's 'version' in '${dir}/package.json'.`); - } - const platform = process.env.npm_config_platform || process.platform; const arch = process.env.npm_config_arch || process.arch; + const listrOptions = { + concurrent: false, + rendererOptions: { + collapseErrors: false, + }, + rendererSilent: !interactive, + rendererFallback: Boolean(process.env.DEBUG && process.env.DEBUG.includes('electron-forge')), + }; - await rebuild(dir, await getElectronVersion(dir, packageJSON), platform as ForgePlatform, arch as ForgeArch, forgeConfig.rebuildConfig); - - await runHook(forgeConfig, 'generateAssets', platform, arch); - + const runner = new Listr( + [ + { + title: 'Locating application', + task: async (ctx) => { + const resolvedDir = await resolveDir(providedDir); + if (!resolvedDir) { + throw new Error('Failed to locate startable Electron application'); + } + ctx.dir = resolvedDir; + }, + }, + { + title: 'Loading configuration', + task: async (ctx) => { + const { dir } = ctx; + ctx.forgeConfig = await getForgeConfig(dir); + ctx.packageJSON = await readMutatedPackageJson(dir, ctx.forgeConfig); + + if (!ctx.packageJSON.version) { + throw new Error(`Please set your application's 'version' in '${dir}/package.json'.`); + } + }, + }, + { + title: 'Rebuilding native modules', + task: async ({ dir, forgeConfig, packageJSON }, task) => { + await listrCompatibleRebuildHook( + dir, + await getElectronVersion(dir, packageJSON), + platform as ForgePlatform, + arch as ForgeArch, + forgeConfig.rebuildConfig, + task + ); + }, + options: { + persistentOutput: true, + bottomBar: Infinity, + showTimer: true, + }, + }, + { + title: 'Generating assets', + task: async ({ forgeConfig }) => { + await runHook(forgeConfig, 'generateAssets', platform, arch); + }, + }, + ], + listrOptions + ); + + await runner.run(); + + const { dir, forgeConfig, packageJSON } = runner.ctx; let lastSpawned: ElectronProcess | null = null; const forgeSpawn = async () => { let electronExecPath: string | null = null; // If a plugin has taken over the start command let's stop here - const spawnedPluginChild = await forgeConfig.pluginInterface.overrideStartLogic({ + let spawnedPluginChild = await forgeConfig.pluginInterface.overrideStartLogic({ dir, appPath, interactive, @@ -72,6 +116,14 @@ export default async ({ inspect, inspectBrk, }); + if (typeof spawnedPluginChild === 'object' && 'tasks' in spawnedPluginChild) { + const innerRunner = new Listr([], listrOptions); + for (const task of spawnedPluginChild.tasks) { + innerRunner.add(task); + } + await innerRunner.run(); + spawnedPluginChild = spawnedPluginChild.result; + } let prefixArgs: string[] = []; if (typeof spawnedPluginChild === 'string') { electronExecPath = spawnedPluginChild; @@ -115,15 +167,11 @@ export default async ({ args = ['--inspect-brk' as string | number].concat(args); } - let spawned!: ElectronProcess; - - await asyncOra('Launching Application', async () => { - spawned = spawn( - electronExecPath!, // eslint-disable-line @typescript-eslint/no-non-null-assertion - prefixArgs.concat([appPath]).concat(args as string[]), - spawnOpts as SpawnOptions - ) as ElectronProcess; - }); + const spawned = spawn( + electronExecPath!, // eslint-disable-line @typescript-eslint/no-non-null-assertion + prefixArgs.concat([appPath]).concat(args as string[]), + spawnOpts as SpawnOptions + ) as ElectronProcess; await runHook(forgeConfig, 'postStart', spawned); return spawned; @@ -165,5 +213,9 @@ export default async ({ process.stdin.resume(); } - return forgeSpawnWrapper(); + const spawned = await forgeSpawnWrapper(); + + console.log(''); + + return spawned; }; diff --git a/packages/api/core/src/util/plugin-interface.ts b/packages/api/core/src/util/plugin-interface.ts index 15555ee365..8233026921 100644 --- a/packages/api/core/src/util/plugin-interface.ts +++ b/packages/api/core/src/util/plugin-interface.ts @@ -9,6 +9,7 @@ import { ResolvedForgeConfig, StartResult, } from '@electron-forge/shared-types'; +import chalk from 'chalk'; import debug from 'debug'; import { StartOptions } from '../api'; @@ -92,7 +93,7 @@ export default class PluginInterface implements IForgePluginInterface { async overrideStartLogic(opts: StartOptions): Promise { let newStartFn; - const claimed = []; + const claimed: string[] = []; for (const plugin of this.plugins) { if (typeof plugin.startLogic === 'function' && plugin.startLogic !== PluginBase.prototype.startLogic) { claimed.push(plugin.name); @@ -104,7 +105,14 @@ export default class PluginInterface implements IForgePluginInterface { } if (claimed.length === 1 && newStartFn) { d(`plugin: "${claimed[0]}" has taken control of the start command`); - return newStartFn(opts); + const result = await newStartFn(opts); + if (typeof result === 'object' && 'tasks' in result) { + result.tasks = result.tasks.map((task) => ({ + ...task, + title: `${chalk.cyan(`[plugin-${claimed[0]}]`)} ${task.title}`, + })); + } + return result; } return false; } diff --git a/packages/api/core/src/util/rebuild.ts b/packages/api/core/src/util/rebuild.ts index dfc2d59877..a4d4c9f4d0 100644 --- a/packages/api/core/src/util/rebuild.ts +++ b/packages/api/core/src/util/rebuild.ts @@ -1,7 +1,81 @@ +import * as cp from 'child_process'; +import * as path from 'path'; + import { asyncOra } from '@electron-forge/async-ora'; -import { ForgeArch, ForgePlatform } from '@electron-forge/shared-types'; +import { ForgeArch, ForgeListrTask, ForgePlatform } from '@electron-forge/shared-types'; import { rebuild, RebuildOptions } from 'electron-rebuild'; +export const listrCompatibleRebuildHook = async ( + buildPath: string, + electronVersion: string, + platform: ForgePlatform, + arch: ForgeArch, + config: Partial = {}, + task: ForgeListrTask +): Promise => { + task.title = 'Preparing native dependencies'; + + const options: RebuildOptions = { + ...config, + buildPath, + electronVersion, + arch, + }; + + const child = cp.fork(path.resolve(__dirname, 'remote-rebuild.js'), [JSON.stringify(options)], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }); + + let pendingError: unknown; + let found = 0; + let done = 0; + + const redraw = () => { + task.title = `Preparing native dependencies: ${done} / ${found}`; + }; + + child.stdout?.on('data', (chunk) => { + task.output = chunk.toString(); + }); + child.stderr?.on('data', (chunk) => { + task.output = chunk.toString(); + }); + + child.on('message', (message: any) => { + switch (message.msg) { + case 'module-found': { + found += 1; + redraw(); + break; + } + case 'module-done': { + done += 1; + redraw(); + break; + } + case 'rebuild-error': { + pendingError = new Error(message.err.message); + (pendingError as any).stack = message.err.stack; + break; + } + case 'rebuild-done': { + task.task.rendererTaskOptions.persistentOutput = false; + break; + } + } + }); + + await new Promise((resolve, reject) => { + child.on('exit', (code) => { + if (code === 0 && !pendingError) { + resolve(); + } else { + reject(pendingError || new Error(`Rebuilder failed with exit code: ${code}`)); + } + }); + }); +}; + export default async ( buildPath: string, electronVersion: string, diff --git a/packages/api/core/src/util/remote-rebuild.ts b/packages/api/core/src/util/remote-rebuild.ts new file mode 100644 index 0000000000..2c793f1c24 --- /dev/null +++ b/packages/api/core/src/util/remote-rebuild.ts @@ -0,0 +1,32 @@ +import { rebuild, RebuildOptions } from 'electron-rebuild'; + +if (!process.send) { + console.error('The remote rebuilder expects to be spawned with an IPC channel'); + // eslint-disable-next-line no-process-exit + process.exit(1); +} + +const options: RebuildOptions = JSON.parse(process.argv[2]); + +const rebuilder = rebuild(options); + +rebuilder.lifecycle.on('module-found', () => process.send!({ msg: 'module-found' })); +rebuilder.lifecycle.on('module-done', () => process.send!({ msg: 'module-done' })); + +rebuilder + .then(() => { + process.send!({ msg: 'rebuild-done' }); + // eslint-disable-next-line no-process-exit + return process.exit(0); + }) + .catch((err) => { + process.send!({ + msg: 'rebuild-error', + err: { + message: err.message, + stack: err.stack, + }, + }); + // eslint-disable-next-line no-process-exit + process.exit(0); + }); diff --git a/packages/api/core/src/util/require-search.ts b/packages/api/core/src/util/require-search.ts index eeaa42bee2..980b899094 100644 --- a/packages/api/core/src/util/require-search.ts +++ b/packages/api/core/src/util/require-search.ts @@ -30,6 +30,17 @@ export function requireSearchRaw(relativeTo: string, paths: string[]): T | nu } } } + // TODO: Is this ok to include in releases? + if (__dirname.includes('forge/packages/api/core/') && paths.length === 1) { + const [moduleType, moduleName] = paths[0].split('/')[1].split('-'); + try { + const localPath = path.resolve(__dirname, '..', '..', '..', '..', moduleType, moduleName); + d('testing local forge build', { moduleType, moduleName, localPath }); + return require(localPath); + } catch { + // Ignore + } + } d('failed to find a module in', testPaths); return null; } diff --git a/packages/plugin/base/src/Plugin.ts b/packages/plugin/base/src/Plugin.ts index 7e5a5df80d..f9479f1b95 100644 --- a/packages/plugin/base/src/Plugin.ts +++ b/packages/plugin/base/src/Plugin.ts @@ -1,4 +1,4 @@ -import { ElectronProcess, ForgeHookMap, IForgePlugin, ResolvedForgeConfig, StartOptions } from '@electron-forge/shared-types'; +import { ForgeHookMap, IForgePlugin, ResolvedForgeConfig, StartOptions, StartResult } from '@electron-forge/shared-types'; export { StartOptions }; @@ -29,7 +29,7 @@ export default abstract class Plugin implements IForgePlugin { return {}; } - async startLogic(_startOpts: StartOptions): Promise { + async startLogic(_startOpts: StartOptions): Promise { return false; } } diff --git a/packages/plugin/webpack/src/WebpackPlugin.ts b/packages/plugin/webpack/src/WebpackPlugin.ts index 2dceaa327d..b7aff8c400 100644 --- a/packages/plugin/webpack/src/WebpackPlugin.ts +++ b/packages/plugin/webpack/src/WebpackPlugin.ts @@ -4,7 +4,7 @@ import path from 'path'; import { asyncOra } from '@electron-forge/async-ora'; import { utils } from '@electron-forge/core'; import { PluginBase } from '@electron-forge/plugin-base'; -import { ForgeHookMap, ResolvedForgeConfig } from '@electron-forge/shared-types'; +import { ForgeHookMap, ResolvedForgeConfig, StartResult } from '@electron-forge/shared-types'; import Logger, { Tab } from '@electron-forge/web-multi-logger'; import chalk from 'chalk'; import debug from 'debug'; @@ -149,8 +149,6 @@ export default class WebpackPlugin extends PluginBase { return this._configGenerator; } - private loggedOutputUrl = false; - getHooks(): ForgeHookMap { return { prePackage: async (config, platform, arch) => { @@ -166,10 +164,6 @@ export default class WebpackPlugin extends PluginBase { await this.compileRenderers(); }, postStart: async (_config, child) => { - if (!this.loggedOutputUrl) { - console.info(`\n\nWebpack Output Available: ${chalk.cyan(`http://localhost:${this.loggerPort}`)}\n`); - this.loggedOutputUrl = true; - } d('hooking electron process exit'); child.on('exit', () => { if (child.restarted) return; @@ -240,36 +234,35 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`); if (logger) { tab = logger.createTab('Main Process'); } - await asyncOra('Compiling Main Process Code', async () => { - const mainConfig = await this.configGenerator.getMainConfig(); - await new Promise((resolve, reject) => { - const compiler = webpack(mainConfig); - const [onceResolve, onceReject] = once(resolve, reject); - const cb: WebpackWatchHandler = async (err, stats) => { - if (tab && stats) { - tab.log( - stats.toString({ - colors: true, - }) - ); - } - if (this.config.jsonStats) { - await this.writeJSONStats('main', stats, mainConfig.stats as WebpackToJsonOptions, 'main'); - } - if (err) return onceReject(err); - if (!watch && stats?.hasErrors()) { - return onceReject(new Error(`Compilation errors in the main process: ${stats.toString()}`)); - } + const mainConfig = await this.configGenerator.getMainConfig(); + await new Promise((resolve, reject) => { + const compiler = webpack(mainConfig); + const [onceResolve, onceReject] = once(resolve, reject); + const cb: WebpackWatchHandler = async (err, stats) => { + if (tab && stats) { + tab.log( + stats.toString({ + colors: true, + }) + ); + } + if (this.config.jsonStats) { + await this.writeJSONStats('main', stats, mainConfig.stats as WebpackToJsonOptions, 'main'); + } - return onceResolve(undefined); - }; - if (watch) { - this.watchers.push(compiler.watch({}, cb)); - } else { - compiler.run(cb); + if (err) return onceReject(err); + if (!watch && stats?.hasErrors()) { + return onceReject(new Error(`Compilation errors in the main process: ${stats.toString()}`)); } - }); + + return onceResolve(undefined); + }; + if (watch) { + this.watchers.push(compiler.watch({}, cb)); + } else { + compiler.run(cb); + } }); }; @@ -298,56 +291,61 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`); }; launchRendererDevServers = async (logger: Logger): Promise => { - await asyncOra('Launching Dev Servers for Renderer Process Code', async () => { - const tab = logger.createTab('Renderers'); - const pluginLogs = new ElectronForgeLoggingPlugin(tab); + const tab = logger.createTab('Renderers'); + const pluginLogs = new ElectronForgeLoggingPlugin(tab); - const config = await this.configGenerator.getRendererConfig(this.config.renderer.entryPoints); + const config = await this.configGenerator.getRendererConfig(this.config.renderer.entryPoints); - if (config.length === 0) { - return; - } + if (config.length === 0) { + return; + } - for (const entryConfig of config) { - if (!entryConfig.plugins) entryConfig.plugins = []; - entryConfig.plugins.push(pluginLogs); - } + for (const entryConfig of config) { + if (!entryConfig.plugins) entryConfig.plugins = []; + entryConfig.plugins.push(pluginLogs); - const compiler = webpack(config); - const webpackDevServer = new WebpackDevServer(this.devServerOptions(), compiler); - await webpackDevServer.start(); - this.servers.push(webpackDevServer.server!); - }); + entryConfig.infrastructureLogging = { + level: 'none', + }; + entryConfig.stats = 'none'; + } - await asyncOra('Compiling Preload Scripts', async () => { - for (const entryPoint of this.config.renderer.entryPoints) { - if ((isLocalWindow(entryPoint) && !!entryPoint.preload) || isPreloadOnly(entryPoint)) { - const config = await this.configGenerator.getPreloadConfigForEntryPoint(entryPoint); - await new Promise((resolve, reject) => { - const tab = logger.createTab(`${entryPoint.name} - Preload`); - const [onceResolve, onceReject] = once(resolve, reject); - - this.watchers.push( - webpack(config).watch({}, (err, stats) => { - if (stats) { - tab.log( - stats.toString({ - colors: true, - }) - ); - } - - if (err) return onceReject(err); - return onceResolve(undefined); - }) - ); - }); - } + const compiler = webpack(config); + const webpackDevServer = new WebpackDevServer(this.devServerOptions(), compiler); + await webpackDevServer.start(); + this.servers.push(webpackDevServer.server!); + + for (const entryPoint of this.config.renderer.entryPoints) { + if ((isLocalWindow(entryPoint) && !!entryPoint.preload) || isPreloadOnly(entryPoint)) { + const config = await this.configGenerator.getPreloadConfigForEntryPoint(entryPoint); + config.infrastructureLogging = { + level: 'none', + }; + config.stats = 'none'; + await new Promise((resolve, reject) => { + const tab = logger.createTab(`${entryPoint.name} - Preload`); + const [onceResolve, onceReject] = once(resolve, reject); + + this.watchers.push( + webpack(config).watch({}, (err, stats) => { + if (stats) { + tab.log( + stats.toString({ + colors: true, + }) + ); + } + + if (err) return onceReject(err); + return onceResolve(undefined); + }) + ); + }); } - }); + } }; - devServerOptions(): Record { + devServerOptions(): WebpackDevServer.Configuration { const cspDirectives = this.config.devContentSecurityPolicy ?? "default-src 'self' 'unsafe-inline' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline' data:"; @@ -372,7 +370,7 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`); private alreadyStarted = false; - async startLogic(): Promise { + async startLogic(): Promise { if (this.alreadyStarted) return false; this.alreadyStarted = true; @@ -380,10 +378,33 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`); const logger = new Logger(this.loggerPort); this.loggers.push(logger); - await this.compileMain(true, logger); - await this.launchRendererDevServers(logger); await logger.start(); - return false; + + return { + tasks: [ + { + title: 'Compiling main process code', + task: async () => { + await this.compileMain(true, logger); + }, + options: { + showTimer: true, + }, + }, + { + title: 'Launching dev servers for renderer process code', + task: async (_, task) => { + await this.launchRendererDevServers(logger); + task.output = `Output Available: ${chalk.cyan(`http://localhost:${this.loggerPort}`)}\n`; + }, + options: { + persistentOutput: true, + showTimer: true, + }, + }, + ], + result: false, + }; } } diff --git a/packages/plugin/webpack/src/util/ElectronForgeLogging.ts b/packages/plugin/webpack/src/util/ElectronForgeLogging.ts index 44c61c7628..b12e14d585 100644 --- a/packages/plugin/webpack/src/util/ElectronForgeLogging.ts +++ b/packages/plugin/webpack/src/util/ElectronForgeLogging.ts @@ -1,52 +1,16 @@ -import { asyncOra } from '@electron-forge/async-ora'; import { Tab } from '@electron-forge/web-multi-logger'; import { Compiler } from 'webpack'; -import once from './once'; - const pluginName = 'ElectronForgeLogging'; export default class LoggingPlugin { tab: Tab; - promiseResolver: (() => void) | undefined; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - promiseRejector: ((reason?: any) => void) | undefined; - constructor(tab: Tab) { this.tab = tab; - this.promiseResolver = undefined; - this.promiseRejector = undefined; - } - - private addRun() { - if (this.promiseResolver) this.promiseResolver(); - asyncOra( - 'Compiling Renderer Code', - () => - new Promise((resolve, reject) => { - const [onceResolve, onceReject] = once(resolve, reject); - this.promiseResolver = onceResolve; - this.promiseRejector = onceReject; - }), - () => { - /* do not exit */ - } - ); - } - - private finishRun(error?: string) { - if (error && this.promiseRejector) this.promiseRejector(error); - else if (this.promiseResolver) this.promiseResolver(); - this.promiseRejector = undefined; - this.promiseResolver = undefined; } apply(compiler: Compiler): void { - compiler.hooks.watchRun.tap(pluginName, (_compiler) => { - this.addRun(); - }); compiler.hooks.done.tap(pluginName, (stats) => { if (stats) { this.tab.log( @@ -54,14 +18,9 @@ export default class LoggingPlugin { colors: true, }) ); - if (stats.hasErrors()) { - this.finishRun(stats.compilation.getErrors().toString()); - return; - } } - this.finishRun(); }); - compiler.hooks.failed.tap(pluginName, (err) => this.finishRun(err.message)); + compiler.hooks.failed.tap(pluginName, (err) => this.tab.log(err.message)); compiler.hooks.infrastructureLog.tap(pluginName, (name: string, _type: string, args: string[]) => { this.tab.log(`${name} - ${args.join(' ')}\n`); return true; diff --git a/packages/template/typescript-webpack/tmpl/tsconfig.json b/packages/template/typescript-webpack/tmpl/tsconfig.json index 3643c33201..92db0d6da1 100644 --- a/packages/template/typescript-webpack/tmpl/tsconfig.json +++ b/packages/template/typescript-webpack/tmpl/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "ES6", "allowJs": true, "module": "commonjs", "skipLibCheck": true, diff --git a/packages/utils/types/src/index.ts b/packages/utils/types/src/index.ts index a44efdca52..70ec27cb66 100644 --- a/packages/utils/types/src/index.ts +++ b/packages/utils/types/src/index.ts @@ -187,13 +187,14 @@ export interface StartOptions { inspectBrk?: boolean; } -export type StartResult = ElectronProcess | string | string[] | false; +export type InnerStartResult = ElectronProcess | string | string[] | false; +export type StartResult = InnerStartResult | { tasks: ForgeListrTaskDefinition[]; result: InnerStartResult }; export interface InitTemplateOptions { copyCIFiles?: boolean; } -export type ForgeListrTaskDefinition = ListrTask; +export type ForgeListrTaskDefinition = ListrTask; export interface ForgeTemplate { requiredForgeVersion?: string; diff --git a/yarn.lock b/yarn.lock index 625998d1c5..40d02d9d1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6979,9 +6979,9 @@ no-case@^3.0.4: tslib "^2.0.3" node-abi@^3.0.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.8.0.tgz#679957dc8e7aa47b0a02589dbfde4f77b29ccb32" - integrity sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw== + version "3.28.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.28.0.tgz#b0df8b317e1c4f2f323756c5fc8ffccc5bca4718" + integrity sha512-fRlDb4I0eLcQeUvGq7IY3xHrSb0c9ummdvDSYWfT9+LKP+3jCKw/tKoqaM7r1BAoiAC6GtwyjaGnOz6B3OtF+A== dependencies: semver "^7.3.5" From 7cd9328effd5ab241ee3ee2824051dcdaee0bdb1 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 30 Oct 2022 15:51:06 -0700 Subject: [PATCH 4/9] refactor: improve unhandled promise logging --- packages/api/cli/src/util/terminate.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/api/cli/src/util/terminate.ts b/packages/api/cli/src/util/terminate.ts index 864d106164..b7c97327fd 100644 --- a/packages/api/cli/src/util/terminate.ts +++ b/packages/api/cli/src/util/terminate.ts @@ -6,10 +6,17 @@ function redConsoleError(msg: string) { process.on('unhandledRejection', (reason: string, promise: Promise) => { redConsoleError('\nAn unhandled rejection has occurred inside Forge:'); - redConsoleError(reason.toString()); + redConsoleError(reason.toString().trim()); 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) => { From 602319d3eed9d5b9958c7ba46f7470074d7a3704 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 30 Oct 2022 16:04:18 -0700 Subject: [PATCH 5/9] chore: fix start and init tests --- packages/api/core/src/api/start.ts | 2 +- packages/api/core/test/fast/start_spec.ts | 4 +++- .../api/core/test/fixture/custom_init/index.js | 14 +++++++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/api/core/src/api/start.ts b/packages/api/core/src/api/start.ts index 3acb16b03f..81190e52cd 100644 --- a/packages/api/core/src/api/start.ts +++ b/packages/api/core/src/api/start.ts @@ -215,7 +215,7 @@ export default async ({ const spawned = await forgeSpawnWrapper(); - console.log(''); + if (interactive) console.log(''); return spawned; }; diff --git a/packages/api/core/test/fast/start_spec.ts b/packages/api/core/test/fast/start_spec.ts index f98b9145ed..bc8a166087 100644 --- a/packages/api/core/test/fast/start_spec.ts +++ b/packages/api/core/test/fast/start_spec.ts @@ -34,7 +34,9 @@ describe('start', () => { '../util/read-package-json': { readMutatedPackageJson: () => Promise.resolve(packageJSON), }, - '../util/rebuild': () => Promise.resolve(), + '../util/rebuild': { + listrCompatibleRebuildHook: () => Promise.resolve(), + }, child_process: { spawn: spawnStub, }, diff --git a/packages/api/core/test/fixture/custom_init/index.js b/packages/api/core/test/fixture/custom_init/index.js index b53d110ff1..8bd44f4be2 100644 --- a/packages/api/core/test/fixture/custom_init/index.js +++ b/packages/api/core/test/fixture/custom_init/index.js @@ -8,8 +8,16 @@ module.exports = { dependencies: ['debug'], devDependencies: ['lodash'], initializeTemplate: async (directory) => { - await baseTemplate.initializeTemplate(directory, {}); - await fs.copy(path.resolve(__dirname, 'tmpl', '_bar'), path.resolve(directory, '.bar')); - await fs.copy(path.resolve(__dirname, 'tmpl', 'src'), path.resolve(directory, 'src')); + const tasks = await baseTemplate.initializeTemplate(directory, {}); + return [ + ...tasks, + { + title: 'Adding custom template files', + task: async () => { + await fs.copy(path.resolve(__dirname, 'tmpl', '_bar'), path.resolve(directory, '.bar')); + await fs.copy(path.resolve(__dirname, 'tmpl', 'src'), path.resolve(directory, 'src')); + }, + }, + ]; }, }; From dd201b72820cde4d3b534daa5bb91630438c0147 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 30 Oct 2022 16:10:43 -0700 Subject: [PATCH 6/9] chore: load local modules first --- packages/api/core/src/util/require-search.ts | 24 +++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/api/core/src/util/require-search.ts b/packages/api/core/src/util/require-search.ts index 980b899094..f326c9d4c2 100644 --- a/packages/api/core/src/util/require-search.ts +++ b/packages/api/core/src/util/require-search.ts @@ -12,6 +12,19 @@ type RequireError = Error & { }; export function requireSearchRaw(relativeTo: string, paths: string[]): T | null { + // Attempt to locally short-circuit if we're running from a checkout of forge + if (__dirname.includes('forge/packages/api/core/') && paths.length === 1 && paths[0].startsWith('@electron-forge/')) { + const [moduleType, moduleName] = paths[0].split('/')[1].split('-'); + try { + const localPath = path.resolve(__dirname, '..', '..', '..', '..', moduleType, moduleName); + d('testing local forge build', { moduleType, moduleName, localPath }); + return require(localPath); + } catch { + // Ignore + } + } + + // Load via normal search paths const testPaths = paths .concat(paths.map((mapPath) => path.resolve(relativeTo, mapPath))) .concat(paths.map((mapPath) => path.resolve(relativeTo, 'node_modules', mapPath))); @@ -30,17 +43,6 @@ export function requireSearchRaw(relativeTo: string, paths: string[]): T | nu } } } - // TODO: Is this ok to include in releases? - if (__dirname.includes('forge/packages/api/core/') && paths.length === 1) { - const [moduleType, moduleName] = paths[0].split('/')[1].split('-'); - try { - const localPath = path.resolve(__dirname, '..', '..', '..', '..', moduleType, moduleName); - d('testing local forge build', { moduleType, moduleName, localPath }); - return require(localPath); - } catch { - // Ignore - } - } d('failed to find a module in', testPaths); return null; } From aa81df23de7ff4a7b8a76291e43848144e677811 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 30 Oct 2022 16:12:14 -0700 Subject: [PATCH 7/9] chore: force ts-checker plugin to pipe to webpack-infra logging channel --- .../template/typescript-webpack/tmpl/webpack.plugins.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/template/typescript-webpack/tmpl/webpack.plugins.js b/packages/template/typescript-webpack/tmpl/webpack.plugins.js index 0aebcd9dce..179104fc32 100644 --- a/packages/template/typescript-webpack/tmpl/webpack.plugins.js +++ b/packages/template/typescript-webpack/tmpl/webpack.plugins.js @@ -1,3 +1,7 @@ const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); -module.exports = [new ForkTsCheckerWebpackPlugin()]; +module.exports = [ + new ForkTsCheckerWebpackPlugin({ + logger: 'webpack-infrastructure', + }), +]; From a8bbf14dc5a8ec718848b460c1be2c0d4146bc8c Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Mon, 31 Oct 2022 13:00:17 -0700 Subject: [PATCH 8/9] chore: handle core-utils migration --- packages/api/core/src/api/init-scripts/init-npm.ts | 2 +- packages/api/core/src/api/init.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 135b309aaa..a6b9bce473 100644 --- a/packages/api/core/src/api/init-scripts/init-npm.ts +++ b/packages/api/core/src/api/init-scripts/init-npm.ts @@ -1,11 +1,11 @@ import path from 'path'; +import { safeYarnOrNpm } from '@electron-forge/core-utils'; import { ForgeListrTask } from '@electron-forge/shared-types'; import debug from 'debug'; import fs from 'fs-extra'; import installDepList, { DepType, DepVersionRestriction } from '../../util/install-dependencies'; -import safeYarnOrNpm from '../../util/yarn-or-npm'; const d = debug('electron-forge:init:npm'); const corePackage = fs.readJsonSync(path.resolve(__dirname, '../../../package.json')); diff --git a/packages/api/core/src/api/init.ts b/packages/api/core/src/api/init.ts index 467bb4b2c6..f880950d5d 100644 --- a/packages/api/core/src/api/init.ts +++ b/packages/api/core/src/api/init.ts @@ -1,5 +1,6 @@ import path from 'path'; +import { safeYarnOrNpm } from '@electron-forge/core-utils'; import { ForgeTemplate } from '@electron-forge/shared-types'; import debug from 'debug'; import { Listr } from 'listr2'; @@ -7,7 +8,6 @@ import semver from 'semver'; import installDepList, { DepType, DepVersionRestriction } from '../util/install-dependencies'; import { readRawPackageJson } from '../util/read-package-json'; -import safeYarnOrNpm from '../util/yarn-or-npm'; import { findTemplate } from './init-scripts/find-template'; import { initDirectory } from './init-scripts/init-directory'; From a1d8678b698ac67c378a895ad833b706008c7df9 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Mon, 31 Oct 2022 13:33:13 -0700 Subject: [PATCH 9/9] chore: move rebuild to core-utils --- packages/api/core/src/api/package.ts | 5 +- packages/api/core/src/api/start.ts | 3 +- packages/api/core/src/util/rebuild.ts | 113 ------------------ packages/api/core/test/fast/start_spec.ts | 4 +- packages/utils/core-utils/src/rebuild.ts | 76 +++++++++++- .../core-utils/src}/remote-rebuild.ts | 0 6 files changed, 79 insertions(+), 122 deletions(-) delete mode 100644 packages/api/core/src/util/rebuild.ts rename packages/{api/core/src/util => utils/core-utils/src}/remote-rebuild.ts (100%) diff --git a/packages/api/core/src/api/package.ts b/packages/api/core/src/api/package.ts index 4a180eeeee..26e0e0067c 100644 --- a/packages/api/core/src/api/package.ts +++ b/packages/api/core/src/api/package.ts @@ -2,7 +2,7 @@ import path from 'path'; import { promisify } from 'util'; import { fakeOra, OraImpl, ora as realOra } from '@electron-forge/async-ora'; -import { getElectronVersion } from '@electron-forge/core-utils'; +import { getElectronVersion, packagerRebuildHook } from '@electron-forge/core-utils'; import { ForgeArch, ForgePlatform } from '@electron-forge/shared-types'; import { getHostArch } from '@electron/get'; import chalk from 'chalk'; @@ -16,7 +16,6 @@ import { runHook } from '../util/hook'; import { warn } from '../util/messages'; import getCurrentOutDir from '../util/out-dir'; import { readMutatedPackageJson } from '../util/read-package-json'; -import rebuildHook from '../util/rebuild'; import requireSearch from '../util/require-search'; import resolveDir from '../util/resolve-dir'; @@ -123,7 +122,7 @@ export default async ({ done(); }, async (buildPath, electronVersion, pPlatform, pArch, done) => { - await rebuildHook(buildPath, electronVersion, pPlatform, pArch, forgeConfig.rebuildConfig); + await packagerRebuildHook(buildPath, electronVersion, pPlatform, pArch, forgeConfig.rebuildConfig); packagerSpinner = ora('Packaging Application').start(); done(); }, diff --git a/packages/api/core/src/api/start.ts b/packages/api/core/src/api/start.ts index ee89f0f814..eec98f4227 100644 --- a/packages/api/core/src/api/start.ts +++ b/packages/api/core/src/api/start.ts @@ -1,6 +1,6 @@ import { spawn, SpawnOptions } from 'child_process'; -import { getElectronVersion } from '@electron-forge/core-utils'; +import { getElectronVersion, listrCompatibleRebuildHook } from '@electron-forge/core-utils'; import { ElectronProcess, ForgeArch, ForgePlatform, ResolvedForgeConfig, StartOptions } from '@electron-forge/shared-types'; import chalk from 'chalk'; import debug from 'debug'; @@ -10,7 +10,6 @@ import locateElectronExecutable from '../util/electron-executable'; import getForgeConfig from '../util/forge-config'; import { runHook } from '../util/hook'; import { readMutatedPackageJson } from '../util/read-package-json'; -import { listrCompatibleRebuildHook } from '../util/rebuild'; import resolveDir from '../util/resolve-dir'; const d = debug('electron-forge:start'); diff --git a/packages/api/core/src/util/rebuild.ts b/packages/api/core/src/util/rebuild.ts deleted file mode 100644 index a4d4c9f4d0..0000000000 --- a/packages/api/core/src/util/rebuild.ts +++ /dev/null @@ -1,113 +0,0 @@ -import * as cp from 'child_process'; -import * as path from 'path'; - -import { asyncOra } from '@electron-forge/async-ora'; -import { ForgeArch, ForgeListrTask, ForgePlatform } from '@electron-forge/shared-types'; -import { rebuild, RebuildOptions } from 'electron-rebuild'; - -export const listrCompatibleRebuildHook = async ( - buildPath: string, - electronVersion: string, - platform: ForgePlatform, - arch: ForgeArch, - config: Partial = {}, - task: ForgeListrTask -): Promise => { - task.title = 'Preparing native dependencies'; - - const options: RebuildOptions = { - ...config, - buildPath, - electronVersion, - arch, - }; - - const child = cp.fork(path.resolve(__dirname, 'remote-rebuild.js'), [JSON.stringify(options)], { - stdio: ['pipe', 'pipe', 'pipe', 'ipc'], - }); - - let pendingError: unknown; - let found = 0; - let done = 0; - - const redraw = () => { - task.title = `Preparing native dependencies: ${done} / ${found}`; - }; - - child.stdout?.on('data', (chunk) => { - task.output = chunk.toString(); - }); - child.stderr?.on('data', (chunk) => { - task.output = chunk.toString(); - }); - - child.on('message', (message: any) => { - switch (message.msg) { - case 'module-found': { - found += 1; - redraw(); - break; - } - case 'module-done': { - done += 1; - redraw(); - break; - } - case 'rebuild-error': { - pendingError = new Error(message.err.message); - (pendingError as any).stack = message.err.stack; - break; - } - case 'rebuild-done': { - task.task.rendererTaskOptions.persistentOutput = false; - break; - } - } - }); - - await new Promise((resolve, reject) => { - child.on('exit', (code) => { - if (code === 0 && !pendingError) { - resolve(); - } else { - reject(pendingError || new Error(`Rebuilder failed with exit code: ${code}`)); - } - }); - }); -}; - -export default async ( - buildPath: string, - electronVersion: string, - platform: ForgePlatform, - arch: ForgeArch, - config: Partial = {} -): Promise => { - await asyncOra('Preparing native dependencies', async (rebuildSpinner) => { - const rebuilder = rebuild({ - ...config, - buildPath, - electronVersion, - arch, - }); - const { lifecycle } = rebuilder; - - let found = 0; - let done = 0; - - const redraw = () => { - rebuildSpinner.text = `Preparing native dependencies: ${done} / ${found}`; - }; - - lifecycle.on('module-found', () => { - found += 1; - redraw(); - }); - lifecycle.on('module-done', () => { - done += 1; - redraw(); - }); - - await rebuilder; - }); -}; diff --git a/packages/api/core/test/fast/start_spec.ts b/packages/api/core/test/fast/start_spec.ts index cc91397532..f90fc6ddd4 100644 --- a/packages/api/core/test/fast/start_spec.ts +++ b/packages/api/core/test/fast/start_spec.ts @@ -23,6 +23,7 @@ describe('start', () => { '../util/electron-executable': () => Promise.resolve('fake_electron_path'), '@electron-forge/core-utils': { getElectronVersion: () => Promise.resolve('1.0.0'), + listrCompatibleRebuildHook: () => Promise.resolve(), }, '../util/forge-config': async () => ({ pluginInterface: { @@ -34,9 +35,6 @@ describe('start', () => { '../util/read-package-json': { readMutatedPackageJson: () => Promise.resolve(packageJSON), }, - '../util/rebuild': { - listrCompatibleRebuildHook: () => Promise.resolve(), - }, child_process: { spawn: spawnStub, }, diff --git a/packages/utils/core-utils/src/rebuild.ts b/packages/utils/core-utils/src/rebuild.ts index abfe6ae853..a56a4901cc 100644 --- a/packages/utils/core-utils/src/rebuild.ts +++ b/packages/utils/core-utils/src/rebuild.ts @@ -1,7 +1,81 @@ +import * as cp from 'child_process'; +import * as path from 'path'; + import { asyncOra } from '@electron-forge/async-ora'; -import { ForgeArch, ForgePlatform } from '@electron-forge/shared-types'; +import { ForgeArch, ForgeListrTask, ForgePlatform } from '@electron-forge/shared-types'; import { rebuild, RebuildOptions } from 'electron-rebuild'; +export const listrCompatibleRebuildHook = async ( + buildPath: string, + electronVersion: string, + platform: ForgePlatform, + arch: ForgeArch, + config: Partial = {}, + task: ForgeListrTask +): Promise => { + task.title = 'Preparing native dependencies'; + + const options: RebuildOptions = { + ...config, + buildPath, + electronVersion, + arch, + }; + + const child = cp.fork(path.resolve(__dirname, 'remote-rebuild.js'), [JSON.stringify(options)], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }); + + let pendingError: unknown; + let found = 0; + let done = 0; + + const redraw = () => { + task.title = `Preparing native dependencies: ${done} / ${found}`; + }; + + child.stdout?.on('data', (chunk) => { + task.output = chunk.toString(); + }); + child.stderr?.on('data', (chunk) => { + task.output = chunk.toString(); + }); + + child.on('message', (message: any) => { + switch (message.msg) { + case 'module-found': { + found += 1; + redraw(); + break; + } + case 'module-done': { + done += 1; + redraw(); + break; + } + case 'rebuild-error': { + pendingError = new Error(message.err.message); + (pendingError as any).stack = message.err.stack; + break; + } + case 'rebuild-done': { + task.task.rendererTaskOptions.persistentOutput = false; + break; + } + } + }); + + await new Promise((resolve, reject) => { + child.on('exit', (code) => { + if (code === 0 && !pendingError) { + resolve(); + } else { + reject(pendingError || new Error(`Rebuilder failed with exit code: ${code}`)); + } + }); + }); +}; + export const packagerRebuildHook = async ( buildPath: string, electronVersion: string, diff --git a/packages/api/core/src/util/remote-rebuild.ts b/packages/utils/core-utils/src/remote-rebuild.ts similarity index 100% rename from packages/api/core/src/util/remote-rebuild.ts rename to packages/utils/core-utils/src/remote-rebuild.ts