From aca89f4c42c92164ef2e0605a140f6340d59cde6 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Thu, 3 Nov 2022 16:01:45 -0700 Subject: [PATCH] refactor: use listr2 for the package, make and publish commands (#3043) * refactor: use listr2 for the package command * chore: fix tests * refactor: use listr2 for the make command * refactor: use listr2 for the publish command * refactor: remove actualTargetPlatform because it is silly * build: shared-types should not depend on ora * spec: fix tests * chore: remove dead code in publish --- .gitignore | 1 + .prettierignore | 1 + package.json | 4 +- packages/api/cli/package.json | 1 - packages/api/core/src/api/index.ts | 4 +- .../src/api/init-scripts/find-template.ts | 2 + .../api/core/src/api/init-scripts/init-npm.ts | 18 +- packages/api/core/src/api/make.ts | 410 ++++++++------ packages/api/core/src/api/package.ts | 505 +++++++++++++----- packages/api/core/src/api/publish.ts | 318 +++++++---- packages/api/core/src/api/start.ts | 16 +- packages/api/core/src/util/hook.ts | 25 + packages/api/core/src/util/index.ts | 4 +- .../api/core/src/util/plugin-interface.ts | 51 +- packages/api/core/test/fast/publish_spec.ts | 28 +- .../core/test/fast/read-package-json_spec.ts | 2 + packages/api/core/test/fast/start_spec.ts | 1 + packages/plugin/base/src/Plugin.ts | 36 +- packages/plugin/compile/package.json | 1 - .../plugin/compile/src/lib/compile-hook.ts | 57 +- packages/plugin/webpack/package.json | 1 - packages/plugin/webpack/src/WebpackPlugin.ts | 71 +-- packages/publisher/base/src/Publisher.ts | 10 +- packages/publisher/bitbucket/package.json | 1 - .../bitbucket/src/PublisherBitbucket.ts | 62 +-- .../electron-release-server/package.json | 1 - .../src/PublisherERS.ts | 80 ++- .../test/PublisherERS_spec.ts | 19 +- packages/publisher/github/package.json | 1 - .../publisher/github/src/PublisherGithub.ts | 129 +++-- packages/publisher/nucleus/package.json | 1 - .../publisher/nucleus/src/PublisherNucleus.ts | 50 +- packages/publisher/s3/package.json | 1 - packages/publisher/s3/src/PublisherS3.ts | 60 +-- packages/publisher/snapcraft/package.json | 1 - .../snapcraft/src/PublisherSnapcraft.ts | 10 +- packages/template/base/package.json | 1 - .../template/webpack-typescript/package.json | 1 - packages/template/webpack/package.json | 1 - packages/utils/core-utils/package.json | 1 - packages/utils/core-utils/src/rebuild.ts | 52 +- .../utils/core-utils/src/remote-rebuild.ts | 8 +- packages/utils/types/package.json | 4 +- packages/utils/types/src/index.ts | 14 +- tools/silent.js | 12 + 45 files changed, 1277 insertions(+), 800 deletions(-) create mode 100644 tools/silent.js diff --git a/.gitignore b/.gitignore index bf7bc5dc51..0e72c4b269 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ packages/**/.swc .webpack packages/api/core/test/fixture/app-with-scoped-name/out/make packages/plugin/webpack/test/fixtures/apps/native-modules/package-lock.json +.links diff --git a/.prettierignore b/.prettierignore index 38f3fe9eb5..da77045cf0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,3 +9,4 @@ packages/*/*/README.md packages/*/*/tsconfig.json packages/api/core/test/fixture/bad_external_forge_config/bad.js packages/plugin/webpack/test/**/.webpack +.links diff --git a/package.json b/package.json index 2a30d6735a..fbf0b1c8e4 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ }, "scripts": { "clean": "rimraf dist && bolt ws exec -- rimraf dist tsconfig.tsbuildinfo", - "build": "bolt ws exec -- tsc --emitDeclarationOnly && bolt ws exec -- swc src --out-dir dist --quiet --extensions \".ts\" --config-file ../../../.swcrc", + "build": "bolt ws exec -- tsc --emitDeclarationOnly && bolt build:fast", + "build:fast": "bolt ws exec -- swc src --out-dir dist --quiet --extensions \".ts\" --config-file ../../../.swcrc", "build:full": "bolt ws exec -- tsc -b", "postbuild": "ts-node tools/test-dist", "coverage:fast": "xvfb-maybe cross-env INTEGRATION_TESTS=0 TS_NODE_PROJECT='./tsconfig.test.json' TS_NODE_FILES=1 nyc mocha './tools/test-globber.ts' && nyc report --reporter=text-lcov > coverage.lcov", @@ -30,6 +31,7 @@ "docs:deploy:build": "bolt docs", "lint": "prettier --check . && eslint .", "lint:fix": "prettier --write .", + "link:prepare": "bolt ws exec -- node ../../../tools/silent.js yarn link --link-folder ../../../.links --silent --no-bin-links", "test": "xvfb-maybe cross-env TS_NODE_PROJECT='./tsconfig.test.json' TS_NODE_FILES=1 mocha './tools/test-globber.ts'", "test:fast": "xvfb-maybe cross-env TS_NODE_PROJECT='./tsconfig.test.json' TEST_FAST_ONLY=1 TS_NODE_FILES=1 mocha './tools/test-globber.ts'", "preinstall": "node ./tools/maybe-shim-windows.js", diff --git a/packages/api/cli/package.json b/packages/api/cli/package.json index 1f96e4e10e..d67a939d33 100644 --- a/packages/api/cli/package.json +++ b/packages/api/cli/package.json @@ -17,7 +17,6 @@ "mocha": "^9.0.1" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/core": "6.0.0", "@electron-forge/shared-types": "6.0.0", "@electron/get": "^2.0.0", diff --git a/packages/api/core/src/api/index.ts b/packages/api/core/src/api/index.ts index 95bbd2edd1..be696fcf2d 100644 --- a/packages/api/core/src/api/index.ts +++ b/packages/api/core/src/api/index.ts @@ -37,8 +37,8 @@ export class ForgeAPI { /** * Resolves hooks if they are a path to a file (instead of a `Function`) */ - package(opts: PackageOptions): Promise { - return _package(opts); + async package(opts: PackageOptions): Promise { + await _package(opts); } /** 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 f256b38230..17d25d8e79 100644 --- a/packages/api/core/src/api/init-scripts/find-template.ts +++ b/packages/api/core/src/api/init-scripts/find-template.ts @@ -35,6 +35,8 @@ export const findTemplate = async (dir: string, template: string): Promise = require(templateModulePath); 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 a6b9bce473..4cced9fe41 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,12 @@ import path from 'path'; -import { safeYarnOrNpm } from '@electron-forge/core-utils'; +import { safeYarnOrNpm, yarnOrNpmSpawn } 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 { readRawPackageJson } from '../../util/read-package-json'; const d = debug('electron-forge:init:npm'); const corePackage = fs.readJsonSync(path.resolve(__dirname, '../../../package.json')); @@ -33,4 +34,19 @@ export const initNPM = async (dir: string, task: ForgeListrTask): Promise { @@ -48,6 +48,14 @@ function isElectronForgeMaker(target: MakerBase | unknown): target is Maker return (target as MakerBase).__isElectronForgeMaker; } +type MakeContext = { + dir: string; + forgeConfig: ResolvedForgeConfig; + actualOutDir: string; + makers: MakerBase[]; + outputs: ForgeMakeResult[]; +}; + export interface MakeOptions { /** * The path to the app from which distrubutables are generated @@ -79,181 +87,253 @@ export interface MakeOptions { outDir?: string; } -export default async ({ - dir = process.cwd(), - interactive = false, - skipPackage = false, - arch = getHostArch() as ForgeArch, - platform = process.platform as ForgePlatform, - overrideTargets, - outDir, -}: MakeOptions): Promise => { - asyncOra.interactive = interactive; - - let forgeConfig!: ResolvedForgeConfig; - await asyncOra('Resolving Forge Config', async () => { - const resolvedDir = await resolveDir(dir); - if (!resolvedDir) { - throw new Error(`Failed to locate makeable Electron application at ${dir}`); - } - dir = resolvedDir; - - forgeConfig = await getForgeConfig(dir); - }); - - const actualOutDir = outDir || getCurrentOutDir(dir, forgeConfig); - - const actualTargetPlatform = platform; - platform = platform === 'mas' ? 'darwin' : platform; - if (!['darwin', 'win32', 'linux', 'mas'].includes(actualTargetPlatform)) { - throw new Error(`'${actualTargetPlatform}' is an invalid platform. Choices are 'darwin', 'mas', 'win32' or 'linux'.`); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const makers: Record> = {}; - - let targets = generateTargets(forgeConfig, overrideTargets); - - let targetId = 0; - for (const target of targets) { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - let maker: MakerBase; - if (isElectronForgeMaker(target)) { - maker = target; - if (!maker.platforms.includes(actualTargetPlatform)) continue; - } else { - const resolvableTarget = target as IForgeResolvableMaker; - // non-false falsy values should be 'true' - if (resolvableTarget.enabled === false) continue; - - if (!resolvableTarget.name) { - throw new Error(`The following maker config is missing a maker name: ${JSON.stringify(resolvableTarget)}`); - } else if (typeof resolvableTarget.name !== 'string') { - throw new Error(`The following maker config has a maker name that is not a string: ${JSON.stringify(resolvableTarget)}`); - } - - const MakerClass = requireSearch(dir, [resolvableTarget.name]); - if (!MakerClass) { - throw new Error( - `Could not find module with name '${resolvableTarget.name}'. If this is a package from NPM, make sure it's listed in the devDependencies of your package.json. If this is a local module, make sure you have the correct path to its entry point. Try using the DEBUG="electron-forge:require-search" environment variable for more information.` - ); - } - - maker = new MakerClass(resolvableTarget.config, resolvableTarget.platforms || undefined); - if (!maker.platforms.includes(actualTargetPlatform)) continue; - } - - if (!maker.isSupportedOnCurrentPlatform) { - throw new Error( - [ - `Maker for target ${maker.name} is incompatible with this version of `, - 'Electron Forge, please upgrade or contact the maintainer ', - "(needs to implement 'isSupportedOnCurrentPlatform)')", - ].join('') - ); - } - - if (!maker.isSupportedOnCurrentPlatform()) { - throw new Error(`Cannot make for ${platform} and target ${maker.name}: the maker declared that it cannot run on ${process.platform}.`); - } +export const listrMake = ( + { + dir: providedDir = process.cwd(), + interactive = false, + skipPackage = false, + arch = getHostArch() as ForgeArch, + platform = process.platform as ForgePlatform, + overrideTargets, + outDir, + }: MakeOptions, + receiveMakeResults?: (results: ForgeMakeResult[]) => void +) => { + const listrOptions = { + concurrent: false, + rendererOptions: { + collapse: false, + collapseErrors: false, + }, + rendererSilent: !interactive, + rendererFallback: Boolean(process.env.DEBUG && process.env.DEBUG.includes('electron-forge')), + }; + + const runner = new Listr( + [ + { + title: 'Loading configuration', + task: async (ctx) => { + const resolvedDir = await resolveDir(providedDir); + if (!resolvedDir) { + throw new Error('Failed to locate startable Electron application'); + } - maker.ensureExternalBinariesExist(); + ctx.dir = resolvedDir; + ctx.forgeConfig = await getForgeConfig(resolvedDir); + }, + }, + { + title: 'Resolving make targets', + task: async (ctx, task) => { + const { dir, forgeConfig } = ctx; + ctx.actualOutDir = outDir || getCurrentOutDir(dir, forgeConfig); + + if (!['darwin', 'win32', 'linux', 'mas'].includes(platform)) { + throw new Error(`'${platform}' is an invalid platform. Choices are 'darwin', 'mas', 'win32' or 'linux'.`); + } - makers[targetId] = maker; - targetId += 1; - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const makers: MakerBase[] = []; - if (!skipPackage) { - info(interactive, chalk.green('We need to package your application before we can make it')); - await packager({ - dir, - interactive, - arch, - outDir: actualOutDir, - platform: actualTargetPlatform, - }); - } else { - warn(interactive, chalk.red('WARNING: Skipping the packaging step, this could result in an out of date build')); - } + const possibleMakers = generateTargets(forgeConfig, overrideTargets); - targets = targets.filter((_, i) => makers[i]); + for (const possibleMaker of possibleMakers) { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + let maker: MakerBase; + if (isElectronForgeMaker(possibleMaker)) { + maker = possibleMaker; + if (!maker.platforms.includes(platform)) continue; + } else { + const resolvableTarget = possibleMaker as IForgeResolvableMaker; + // non-false falsy values should be 'true' + if (resolvableTarget.enabled === false) continue; + + if (!resolvableTarget.name) { + throw new Error(`The following maker config is missing a maker name: ${JSON.stringify(resolvableTarget)}`); + } else if (typeof resolvableTarget.name !== 'string') { + throw new Error(`The following maker config has a maker name that is not a string: ${JSON.stringify(resolvableTarget)}`); + } + + const MakerClass = requireSearch(dir, [resolvableTarget.name]); + if (!MakerClass) { + throw new Error( + `Could not find module with name '${resolvableTarget.name}'. If this is a package from NPM, make sure it's listed in the devDependencies of your package.json. If this is a local module, make sure you have the correct path to its entry point. Try using the DEBUG="electron-forge:require-search" environment variable for more information.` + ); + } + + maker = new MakerClass(resolvableTarget.config, resolvableTarget.platforms || undefined); + if (!maker.platforms.includes(platform)) continue; + } - if (targets.length === 0) { - throw new Error(`Could not find any make targets configured for the "${actualTargetPlatform}" platform.`); - } + if (!maker.isSupportedOnCurrentPlatform) { + throw new Error( + [ + `Maker for target ${maker.name} is incompatible with this version of `, + 'Electron Forge, please upgrade or contact the maintainer ', + "(needs to implement 'isSupportedOnCurrentPlatform)')", + ].join('') + ); + } - info(interactive, `Making for the following targets: ${chalk.cyan(`${targets.map((_t, i) => makers[i].name).join(', ')}`)}`); + if (!maker.isSupportedOnCurrentPlatform()) { + throw new Error(`Cannot make for ${platform} and target ${maker.name}: the maker declared that it cannot run on ${process.platform}.`); + } - const packageJSON = await readMutatedPackageJson(dir, forgeConfig); - const appName = filenamify(forgeConfig.packagerConfig.name || packageJSON.productName || packageJSON.name, { replacement: '-' }); - const outputs: ForgeMakeResult[] = []; + maker.ensureExternalBinariesExist(); - await runHook(forgeConfig, 'preMake'); + makers.push(maker); + } - for (const targetArch of parseArchs(platform, arch, await getElectronVersion(dir, packageJSON))) { - const packageDir = path.resolve(actualOutDir, `${appName}-${actualTargetPlatform}-${targetArch}`); - if (!(await fs.pathExists(packageDir))) { - throw new Error(`Couldn't find packaged app at: ${packageDir}`); - } + if (makers.length === 0) { + throw new Error(`Could not find any make targets configured for the "${platform}" platform.`); + } - targetId = 0; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const _target of targets) { - const maker = makers[targetId]; - targetId += 1; - - await asyncOra( - `Making for target: ${chalk.green(maker.name)} - On platform: ${chalk.cyan(actualTargetPlatform)} - For arch: ${chalk.cyan(targetArch)}`, - async () => { - try { - /** - * WARNING: DO NOT ATTEMPT TO PARALLELIZE MAKERS - * - * Currently it is assumed we have 1 maker per make call but that is - * not enforced. It is technically possible to have 1 maker be called - * multiple times. The "prepareConfig" method however implicitly - * requires a lock that is not enforced. There are two options: - * - * * Provide makers a getConfig() method - * * Remove support for config being provided as a method - * * Change the entire API of maker from a single constructor to - * providing a MakerFactory - */ - maker.prepareConfig(targetArch); - const artifacts = await maker.make({ - appName, - forgeConfig, - packageJSON, - targetArch, - dir: packageDir, - makeDir: path.resolve(actualOutDir, 'make'), - targetPlatform: actualTargetPlatform, + ctx.makers = makers; + + task.output = `Making for the following targets: ${chalk.magenta(`${makers.map((maker) => maker.name).join(', ')}`)}`; + }, + options: { + persistentOutput: true, + }, + }, + { + title: `Running ${chalk.yellow('package')} command`, + task: async (ctx, task) => { + if (!skipPackage) { + return listrPackage({ + dir: ctx.dir, + interactive, + arch, + outDir: ctx.actualOutDir, + platform, }); + } else { + task.output = chalk.yellow(`${logSymbols.warning} Skipping could result in an out of date build`); + task.skip(); + } + }, + options: { + persistentOutput: true, + }, + }, + { + title: `Running ${chalk.yellow('preMake')} hook`, + task: async (ctx, task) => { + return task.newListr(await getHookListrTasks(ctx.forgeConfig, 'preMake')); + }, + }, + { + title: 'Making distributables', + task: async (ctx, task) => { + const { actualOutDir, dir, forgeConfig, makers } = ctx; + const packageJSON = await readMutatedPackageJson(dir, forgeConfig); + const appName = filenamify(forgeConfig.packagerConfig.name || packageJSON.productName || packageJSON.name, { replacement: '-' }); + const outputs: ForgeMakeResult[] = []; + ctx.outputs = outputs; + + const subRunner = task.newListr([], { + ...listrOptions, + rendererOptions: { + collapse: false, + collapseErrors: false, + }, + }); + + for (const targetArch of parseArchs(platform, arch, await getElectronVersion(dir, packageJSON))) { + const packageDir = path.resolve(actualOutDir, `${appName}-${platform}-${targetArch}`); + if (!(await fs.pathExists(packageDir))) { + throw new Error(`Couldn't find packaged app at: ${packageDir}`); + } - outputs.push({ - artifacts, - packageJSON, - platform: actualTargetPlatform, - arch: targetArch, - }); - } catch (err) { - if (err instanceof Error) { - throw { - message: `An error occured while making for target: ${maker.name}`, - stack: `${err.message}\n${err.stack}`, - }; - } else if (err) { - throw err; - } else { - throw new Error(`An unknown error occured while making for target: ${maker.name}`); + for (const maker of makers) { + subRunner.add({ + title: `Making a ${chalk.magenta(maker.name)} distributable for ${chalk.cyan(`${platform}/${targetArch}`)}`, + task: async () => { + try { + /** + * WARNING: DO NOT ATTEMPT TO PARALLELIZE MAKERS + * + * Currently it is assumed we have 1 maker per make call but that is + * not enforced. It is technically possible to have 1 maker be called + * multiple times. The "prepareConfig" method however implicitly + * requires a lock that is not enforced. There are two options: + * + * * Provide makers a getConfig() method + * * Remove support for config being provided as a method + * * Change the entire API of maker from a single constructor to + * providing a MakerFactory + */ + maker.prepareConfig(targetArch); + const artifacts = await maker.make({ + appName, + forgeConfig, + packageJSON, + targetArch, + dir: packageDir, + makeDir: path.resolve(actualOutDir, 'make'), + targetPlatform: platform, + }); + + outputs.push({ + artifacts, + packageJSON, + platform, + arch: targetArch, + }); + } catch (err) { + if (err instanceof Error) { + throw { + message: `An error occured while making for target: ${maker.name}`, + stack: `${err.message}\n${err.stack}`, + }; + } else if (err) { + throw err; + } else { + throw new Error(`An unknown error occured while making for target: ${maker.name}`); + } + } + }, + options: { + showTimer: true, + }, + }); } } - } - ); + + return subRunner; + }, + }, + { + title: `Running ${chalk.yellow('postMake')} hook`, + task: async (ctx, task) => { + // If the postMake hooks modifies the locations / names of the outputs it must return + // the new locations so that the publish step knows where to look + ctx.outputs = await runMutatingHook(ctx.forgeConfig, 'postMake', ctx.outputs); + receiveMakeResults?.(ctx.outputs); + + task.output = `Artifacts available at: ${chalk.green(path.resolve(ctx.actualOutDir, 'make'))}`; + }, + options: { + persistentOutput: true, + }, + }, + ], + { + ...listrOptions, + ctx: {} as MakeContext, } - } + ); - // If the postMake hooks modifies the locations / names of the outputs it must return - // the new locations so that the publish step knows where to look - return runMutatingHook(forgeConfig, 'postMake', outputs); + return runner; }; + +const make = async (opts: MakeOptions): Promise => { + const runner = listrMake(opts); + + await runner.run(); + + return runner.ctx.outputs; +}; + +export default make; diff --git a/packages/api/core/src/api/package.ts b/packages/api/core/src/api/package.ts index 7f051c9c5c..6b704a3e7a 100644 --- a/packages/api/core/src/api/package.ts +++ b/packages/api/core/src/api/package.ts @@ -1,18 +1,18 @@ import path from 'path'; import { promisify } from 'util'; -import { fakeOra, ora as realOra } from '@electron-forge/async-ora'; -import { getElectronVersion, packagerRebuildHook } from '@electron-forge/core-utils'; -import { ForgeArch, ForgePlatform } from '@electron-forge/shared-types'; +import { getElectronVersion, listrCompatibleRebuildHook } from '@electron-forge/core-utils'; +import { ForgeArch, ForgeListrTask, ForgeListrTaskDefinition, ForgePlatform, ResolvedForgeConfig } from '@electron-forge/shared-types'; import { getHostArch } from '@electron/get'; import chalk from 'chalk'; import debug from 'debug'; import packager, { FinalizePackageTargetsHookFunction, HookFunction, TargetDefinition } from 'electron-packager'; import glob from 'fast-glob'; import fs from 'fs-extra'; +import { Listr } from 'listr2'; import getForgeConfig from '../util/forge-config'; -import { runHook } from '../util/hook'; +import { getHookListrTasks, runHook } from '../util/hook'; import { warn } from '../util/messages'; import getCurrentOutDir from '../util/out-dir'; import { readMutatedPackageJson } from '../util/read-package-json'; @@ -47,6 +47,7 @@ function sequentialHooks(hooks: HookFunction[]): PromisifiedHookFunction[] { try { await promisify(hook)(buildPath, electronVersion, platform, arch); } catch (err) { + d('hook failed:', hook.toString(), err); return done(err as Error); } } @@ -69,6 +70,23 @@ function sequentialFinalizePackageTargetsHooks(hooks: FinalizePackageTargetsHook ] as PromisifiedFinalizePackageTargetsHookFunction[]; } +type PackageContext = { + dir: string; + forgeConfig: ResolvedForgeConfig; + packageJSON: any; + calculatedOutDir: string; + packagerPromise: Promise; + targets: InternalTargetDefinition[]; +}; + +type InternalTargetDefinition = TargetDefinition & { + forUniversal?: boolean; +}; + +type PackageResult = TargetDefinition & { + packagedPath: string; +}; + export interface PackageOptions { /** * The path to the app to package @@ -92,154 +110,347 @@ export interface PackageOptions { outDir?: string; } -export default async ({ - dir = process.cwd(), +export const listrPackage = ({ + dir: providedDir = process.cwd(), interactive = false, arch = getHostArch() as ForgeArch, platform = process.platform as ForgePlatform, outDir, -}: PackageOptions): Promise => { - const ora = interactive ? realOra : fakeOra; - - let spinner = ora(`Preparing to Package Application`).start(); - - const resolvedDir = await resolveDir(dir); - if (!resolvedDir) { - throw new Error('Failed to locate compilable Electron application'); - } - dir = resolvedDir; - - const forgeConfig = await getForgeConfig(dir); - const packageJSON = await readMutatedPackageJson(dir, forgeConfig); - - if (!packageJSON.main) { - throw new Error('packageJSON.main must be set to a valid entry point for your Electron app'); - } - - const calculatedOutDir = outDir || getCurrentOutDir(dir, forgeConfig); - - let pending: TargetDefinition[] = []; - - function readableTargets(targets: TargetDefinition[]) { - return targets.map(({ platform, arch }) => `${platform}:${arch}`).join(', '); - } - - const afterFinalizePackageTargetsHooks: FinalizePackageTargetsHookFunction[] = [ - (matrix, done) => { - spinner.succeed(); - spinner = ora(`Packaging for ${chalk.cyan(readableTargets(matrix))}`).start(); - pending.push(...matrix); - done(); - }, - ...resolveHooks(forgeConfig.packagerConfig.afterFinalizePackageTargets, dir), - ]; - - const pruneEnabled = !('prune' in forgeConfig.packagerConfig) || forgeConfig.packagerConfig.prune; - - const afterCopyHooks: HookFunction[] = [ - async (buildPath, electronVersion, pPlatform, pArch, done) => { - const bins = await glob(path.join(buildPath, '**/.bin/**/*')); - for (const bin of bins) { - await fs.remove(bin); - } - done(); - }, - async (buildPath, electronVersion, pPlatform, pArch, done) => { - await runHook(forgeConfig, 'packageAfterCopy', buildPath, electronVersion, pPlatform, pArch); - done(); - }, - async (buildPath, electronVersion, pPlatform, pArch, done) => { - await packagerRebuildHook(buildPath, electronVersion, pPlatform, pArch, forgeConfig.rebuildConfig); - done(); - }, - async (buildPath, electronVersion, pPlatform, pArch, done) => { - const copiedPackageJSON = await readMutatedPackageJson(buildPath, forgeConfig); - if (copiedPackageJSON.config && copiedPackageJSON.config.forge) { - delete copiedPackageJSON.config.forge; - } - await fs.writeJson(path.resolve(buildPath, 'package.json'), copiedPackageJSON, { spaces: 2 }); - done(); - }, - ...resolveHooks(forgeConfig.packagerConfig.afterCopy, dir), - async (buildPath, electronVersion, pPlatform, pArch, done) => { - spinner.text = `Packaging for ${chalk.cyan(pArch)} complete`; - spinner.succeed(); - pending = pending.filter(({ arch, platform }) => !(arch === pArch && platform === pPlatform)); - if (pending.length > 0) { - spinner = ora(`Packaging for ${chalk.cyan(readableTargets(pending))}`).start(); - } else { - spinner = ora(`Packaging complete`).start(); - } - - done(); - }, - ]; - - const afterPruneHooks = []; - - if (pruneEnabled) { - afterPruneHooks.push(...resolveHooks(forgeConfig.packagerConfig.afterPrune, dir)); - } - - afterPruneHooks.push((async (buildPath, electronVersion, pPlatform, pArch, done) => { - await runHook(forgeConfig, 'packageAfterPrune', buildPath, electronVersion, pPlatform, pArch); - done(); - }) as HookFunction); - - const afterExtractHooks = [ - (async (buildPath, electronVersion, pPlatform, pArch, done) => { - await runHook(forgeConfig, 'packageAfterExtract', buildPath, electronVersion, pPlatform, pArch); - done(); - }) as HookFunction, - ]; - afterExtractHooks.push(...resolveHooks(forgeConfig.packagerConfig.afterExtract, dir)); - - type PackagerArch = Exclude; - - const packageOpts: packager.Options = { - asar: false, - overwrite: true, - ...forgeConfig.packagerConfig, - dir, - arch: arch as PackagerArch, - platform, - afterFinalizePackageTargets: sequentialFinalizePackageTargetsHooks(afterFinalizePackageTargetsHooks), - afterCopy: sequentialHooks(afterCopyHooks), - afterExtract: sequentialHooks(afterExtractHooks), - afterPrune: sequentialHooks(afterPruneHooks), - out: calculatedOutDir, - electronVersion: await getElectronVersion(dir, packageJSON), - }; - packageOpts.quiet = true; - - if (packageOpts.all) { - throw new Error('config.forge.packagerConfig.all is not supported by Electron Forge'); - } - - if (!packageJSON.version && !packageOpts.appVersion) { - warn( - interactive, - chalk.yellow('Please set "version" or "config.forge.packagerConfig.appVersion" in your application\'s package.json so auto-updates work properly') - ); - } - - if (packageOpts.prebuiltAsar) { - throw new Error('config.forge.packagerConfig.prebuiltAsar is not supported by Electron Forge'); - } - - await runHook(forgeConfig, 'generateAssets', platform, arch); - await runHook(forgeConfig, 'prePackage', platform, arch); - - d('packaging with options', packageOpts); +}: PackageOptions) => { + const runner = new Listr( + [ + { + title: 'Preparing to package application', + task: async (ctx) => { + const resolvedDir = await resolveDir(providedDir); + if (!resolvedDir) { + throw new Error('Failed to locate compilable Electron application'); + } + ctx.dir = resolvedDir; + + ctx.forgeConfig = await getForgeConfig(resolvedDir); + ctx.packageJSON = await readMutatedPackageJson(resolvedDir, ctx.forgeConfig); + + if (!ctx.packageJSON.main) { + throw new Error('packageJSON.main must be set to a valid entry point for your Electron app'); + } + + ctx.calculatedOutDir = outDir || getCurrentOutDir(resolvedDir, ctx.forgeConfig); + }, + }, + { + title: 'Running packaging hooks', + task: async ({ forgeConfig }, task) => { + return task.newListr([ + { + title: `Running ${chalk.yellow('generateAssets')} hook`, + task: async (_, task) => { + return task.newListr(await getHookListrTasks(forgeConfig, 'generateAssets', platform, arch)); + }, + }, + { + title: `Running ${chalk.yellow('prePackage')} hook`, + task: async (_, task) => { + return task.newListr(await getHookListrTasks(forgeConfig, 'prePackage', platform, arch)); + }, + }, + ]); + }, + }, + { + title: 'Packaging application', + task: async (ctx, task) => { + const { calculatedOutDir, forgeConfig, packageJSON } = ctx; + const getTargetKey = (target: TargetDefinition) => `${target.platform}/${target.arch}`; + + task.output = 'Determining targets...'; + + let provideTargets: (targets: TargetDefinition[]) => void; + const targetsPromise = new Promise((resolve) => { + provideTargets = resolve; + }); + + type StepDoneSignalMap = Map void)[]>; + const signalCopyDone: StepDoneSignalMap = new Map(); + const signalRebuildDone: StepDoneSignalMap = new Map(); + const signalPackageDone: StepDoneSignalMap = new Map(); + const rejects: ((err: any) => void)[] = []; + const signalDone = (map: StepDoneSignalMap, target: TargetDefinition) => { + map.get(getTargetKey(target))?.pop()?.(); + }; + const addSignalAndWait = async (map: StepDoneSignalMap, target: TargetDefinition) => { + const targetKey = getTargetKey(target); + await new Promise((resolve, reject) => { + rejects.push(reject); + map.set(targetKey, (map.get(targetKey) || []).concat([resolve])); + }); + }; + + const rebuildTasks = new Map>[]>(); + const signalRebuildStart = new Map) => void)[]>(); + + const afterFinalizePackageTargetsHooks: FinalizePackageTargetsHookFunction[] = [ + (targets, done) => { + provideTargets(targets); + done(); + }, + ...resolveHooks(forgeConfig.packagerConfig.afterFinalizePackageTargets, ctx.dir), + ]; + + const pruneEnabled = !('prune' in forgeConfig.packagerConfig) || forgeConfig.packagerConfig.prune; + + const afterCopyHooks: HookFunction[] = [ + async (buildPath, electronVersion, platform, arch, done) => { + signalDone(signalCopyDone, { platform, arch }); + done(); + }, + async (buildPath, electronVersion, pPlatform, pArch, done) => { + const bins = await glob(path.join(buildPath, '**/.bin/**/*')); + for (const bin of bins) { + await fs.remove(bin); + } + done(); + }, + async (buildPath, electronVersion, pPlatform, pArch, done) => { + await runHook(forgeConfig, 'packageAfterCopy', buildPath, electronVersion, pPlatform, pArch); + done(); + }, + async (buildPath, electronVersion, pPlatform, pArch, done) => { + const targetKey = getTargetKey({ platform: pPlatform, arch: pArch }); + await listrCompatibleRebuildHook( + buildPath, + electronVersion, + pPlatform, + pArch, + forgeConfig.rebuildConfig, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await rebuildTasks.get(targetKey)!.pop()! + ); + signalRebuildDone.get(targetKey)?.pop()?.(); + done(); + }, + async (buildPath, electronVersion, pPlatform, pArch, done) => { + const copiedPackageJSON = await readMutatedPackageJson(buildPath, forgeConfig); + if (copiedPackageJSON.config && copiedPackageJSON.config.forge) { + delete copiedPackageJSON.config.forge; + } + await fs.writeJson(path.resolve(buildPath, 'package.json'), copiedPackageJSON, { spaces: 2 }); + done(); + }, + ...resolveHooks(forgeConfig.packagerConfig.afterCopy, ctx.dir), + ]; + + const afterCompleteHooks: HookFunction[] = [ + async (buildPath, electronVersion, pPlatform, pArch, done) => { + signalPackageDone.get(getTargetKey({ platform: pPlatform, arch: pArch }))?.pop()?.(); + done(); + }, + ]; + + const afterPruneHooks = []; + + if (pruneEnabled) { + afterPruneHooks.push(...resolveHooks(forgeConfig.packagerConfig.afterPrune, ctx.dir)); + } + + afterPruneHooks.push((async (buildPath, electronVersion, pPlatform, pArch, done) => { + await runHook(forgeConfig, 'packageAfterPrune', buildPath, electronVersion, pPlatform, pArch); + done(); + }) as HookFunction); + + const afterExtractHooks = [ + (async (buildPath, electronVersion, pPlatform, pArch, done) => { + await runHook(forgeConfig, 'packageAfterExtract', buildPath, electronVersion, pPlatform, pArch); + done(); + }) as HookFunction, + ]; + afterExtractHooks.push(...resolveHooks(forgeConfig.packagerConfig.afterExtract, ctx.dir)); + + type PackagerArch = Exclude; + + const packageOpts: packager.Options = { + asar: false, + overwrite: true, + ignore: [/^\/out\//g], + ...forgeConfig.packagerConfig, + quiet: true, + dir: ctx.dir, + arch: arch as PackagerArch, + platform, + afterFinalizePackageTargets: sequentialFinalizePackageTargetsHooks(afterFinalizePackageTargetsHooks), + afterComplete: sequentialHooks(afterCompleteHooks), + afterCopy: sequentialHooks(afterCopyHooks), + afterExtract: sequentialHooks(afterExtractHooks), + afterPrune: sequentialHooks(afterPruneHooks), + out: calculatedOutDir, + electronVersion: await getElectronVersion(ctx.dir, packageJSON), + }; + packageOpts.quiet = true; + + if (packageOpts.all) { + throw new Error('config.forge.packagerConfig.all is not supported by Electron Forge'); + } + + if (!packageJSON.version && !packageOpts.appVersion) { + warn( + interactive, + chalk.yellow('Please set "version" or "config.forge.packagerConfig.appVersion" in your application\'s package.json so auto-updates work properly') + ); + } + + if (packageOpts.prebuiltAsar) { + throw new Error('config.forge.packagerConfig.prebuiltAsar is not supported by Electron Forge'); + } + + d('packaging with options', packageOpts); + + ctx.packagerPromise = packager(packageOpts); + // Handle error by failing this task + // rejects is populated by the reject handlers for every + // signal based promise in every subtask + ctx.packagerPromise.catch((err) => { + for (const reject of rejects) reject(err); + }); + + const targets = await targetsPromise; + // Copy the resolved targets into the context for later + ctx.targets = [...targets]; + // If we are targetting a universal build we need to add the "fake" + // x64 and arm64 builds into the list of targets so that we can + // show progress for those + for (const target of targets) { + if (target.arch === 'universal') { + targets.push( + { + platform: target.platform, + arch: 'x64', + forUniversal: true, + }, + { + platform: target.platform, + arch: 'arm64', + forUniversal: true, + } + ); + } + } + + // Populate rebuildTasks with promises that resolve with the rebuild tasks + // that will eventually run + for (const target of targets) { + // Skip universal tasks as they do not have rebuild sub-tasks + if (target.arch === 'universal') continue; + + const targetKey = getTargetKey(target); + rebuildTasks.set( + targetKey, + (rebuildTasks.get(targetKey) || []).concat([ + new Promise((resolve) => { + signalRebuildStart.set(targetKey, (signalRebuildStart.get(targetKey) || []).concat([resolve])); + }), + ]) + ); + } + d('targets:', targets); + + return task.newListr( + targets.map( + (target): ForgeListrTaskDefinition => + target.arch === 'universal' + ? { + title: `Stitching ${chalk.cyan(`${target.platform}/x64`)} and ${chalk.cyan(`${target.platform}/arm64`)} into a ${chalk.green( + `${target.platform}/universal` + )} package`, + task: async () => { + await addSignalAndWait(signalPackageDone, target); + }, + options: { + showTimer: true, + }, + } + : { + title: `Packaging for ${chalk.cyan(target.arch)} on ${chalk.cyan(target.platform)}${ + target.forUniversal ? chalk.italic(' (for universal package)') : '' + }`, + task: async (_, task) => { + return task.newListr( + [ + { + title: 'Copying files', + task: async () => { + await addSignalAndWait(signalCopyDone, target); + }, + }, + { + title: 'Preparing native dependencies', + task: async (_, task) => { + signalRebuildStart.get(getTargetKey(target))?.pop()?.(task); + await addSignalAndWait(signalRebuildDone, target); + }, + options: { + persistentOutput: true, + bottomBar: Infinity, + showTimer: true, + }, + }, + { + title: 'Finalizing package', + task: async () => { + await addSignalAndWait(signalPackageDone, target); + }, + }, + ], + { rendererOptions: { collapse: true, collapseErrors: false } } + ); + }, + options: { + showTimer: true, + }, + } + ), + { concurrent: true, rendererOptions: { collapse: false, collapseErrors: false } } + ); + }, + }, + { + title: `Running ${chalk.yellow('postPackage')} hook`, + task: async ({ packagerPromise, forgeConfig }, task) => { + const outputPaths = await packagerPromise; + d('outputPaths:', outputPaths); + return task.newListr( + await getHookListrTasks(forgeConfig, 'postPackage', { + arch, + outputPaths, + platform, + }) + ); + }, + }, + ], + { + concurrent: false, + rendererSilent: !interactive, + rendererFallback: Boolean(process.env.DEBUG && process.env.DEBUG.includes('electron-forge')), + rendererOptions: { + collapse: false, + collapseErrors: false, + }, + ctx: {} as PackageContext, + } + ); + + return runner; +}; - const outputPaths = await packager(packageOpts); +export default async (opts: PackageOptions): Promise => { + const runner = listrPackage(opts); - await runHook(forgeConfig, 'postPackage', { - arch, - outputPaths, - platform, - spinner, - }); + await runner.run(); - if (spinner) spinner.succeed(); + const outputPaths = await runner.ctx.packagerPromise; + return runner.ctx.targets.map((target, index) => ({ + platform: target.platform, + arch: target.arch, + packagedPath: outputPaths[index], + })); }; diff --git a/packages/api/core/src/api/publish.ts b/packages/api/core/src/api/publish.ts index c249b0a125..6ffda99c0c 100644 --- a/packages/api/core/src/api/publish.ts +++ b/packages/api/core/src/api/publish.ts @@ -1,17 +1,19 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; import { PublisherBase } from '@electron-forge/publisher-base'; import { ForgeConfigPublisher, + ForgeListrTask, ForgeMakeResult, IForgePublisher, IForgeResolvablePublisher, + ResolvedForgeConfig, // ForgePlatform, } from '@electron-forge/shared-types'; import chalk from 'chalk'; import debug from 'debug'; import fs from 'fs-extra'; +import { Listr } from 'listr2'; import getForgeConfig from '../util/forge-config'; import getCurrentOutDir from '../util/out-dir'; @@ -19,10 +21,17 @@ import PublishState from '../util/publish-state'; import requireSearch from '../util/require-search'; import resolveDir from '../util/resolve-dir'; -import make, { MakeOptions } from './make'; +import { listrMake, MakeOptions } from './make'; const d = debug('electron-forge:publish'); +type PublishContext = { + dir: string; + forgeConfig: ResolvedForgeConfig; + publishers: PublisherBase[]; + makeResults: ForgeMakeResult[]; +}; + export interface PublishOptions { /** * The path to the app to be published @@ -55,137 +64,224 @@ export interface PublishOptions { * You can't use this combination at the same time as dryRun=true */ dryRunResume?: boolean; - /** - * Provide results from make so that the publish step doesn't run make itself - */ - makeResults?: ForgeMakeResult[]; } const publish = async ({ - dir = process.cwd(), + dir: providedDir = process.cwd(), interactive = false, makeOptions = {}, publishTargets = undefined, dryRun = false, dryRunResume = false, - makeResults = undefined, outDir, }: PublishOptions): Promise => { - asyncOra.interactive = interactive; - if (dryRun && dryRunResume) { throw new Error("Can't dry run and resume a dry run at the same time"); } - if (dryRunResume && makeResults) { - throw new Error("Can't resume a dry run and use the provided makeResults at the same time"); - } - const forgeConfig = await getForgeConfig(dir); - - const calculatedOutDir = outDir || getCurrentOutDir(dir, forgeConfig); - const dryRunDir = path.resolve(calculatedOutDir, 'publish-dry-run'); - - if (dryRunResume) { - d('attempting to resume from dry run'); - const publishes = await PublishState.loadFromDirectory(dryRunDir, dir); - for (const publishStates of publishes) { - d('publishing for given state set'); - await publish({ - dir, - interactive, - publishTargets, - makeOptions, - dryRun: false, - dryRunResume: false, - makeResults: publishStates.map(({ state }) => state), - }); - } - return; - } + const listrOptions = { + concurrent: false, + rendererOptions: { + collapseErrors: false, + }, + rendererSilent: !interactive, + rendererFallback: Boolean(process.env.DEBUG && process.env.DEBUG.includes('electron-forge')), + }; - if (!makeResults) { - d('triggering make'); - makeResults = await make({ - dir, - interactive, - ...makeOptions, - }); - } else { - // Restore values from dry run - d('restoring publish settings from dry run'); - - for (const makeResult of makeResults) { - makeOptions.platform = makeResult.platform; - makeOptions.arch = makeResult.arch; - - for (const makePath of makeResult.artifacts) { - if (!(await fs.pathExists(makePath))) { - throw new Error(`Attempted to resume a dry run but an artifact (${makePath}) could not be found`); + const publishDistributablesTasks = [ + { + title: 'Publishing distributables', + task: async ({ dir, forgeConfig, makeResults, publishers }: PublishContext, task: ForgeListrTask) => { + if (publishers.length === 0) { + task.output = 'No publishers configured'; + task.skip(); + return; } - } - } - } - if (dryRun) { - d('saving results of make in dry run state', makeResults); - await fs.remove(dryRunDir); - await PublishState.saveToDirectory(dryRunDir, makeResults, dir); - return; - } + return task.newListr( + publishers.map((publisher) => ({ + title: `${chalk.cyan(`[publisher-${publisher.name}]`)} Running the ${chalk.yellow('publish')} command`, + task: async (_, task) => { + const setStatusLine = (s: string) => { + task.output = s; + }; + await publisher.publish({ + dir, + makeResults: makeResults!, + forgeConfig, + setStatusLine, + }); + }, + options: { + persistentOutput: true, + }, + })), + { + rendererOptions: { + collapse: false, + collapseErrors: false, + }, + } + ); + }, + options: { + persistentOutput: true, + }, + }, + ]; - const resolvedDir = await resolveDir(dir); - if (!resolvedDir) { - throw new Error('Failed to locate publishable Electron application'); - } - dir = resolvedDir; + const runner = new Listr( + [ + { + title: 'Loading configuration', + task: async (ctx) => { + const resolvedDir = await resolveDir(providedDir); + if (!resolvedDir) { + throw new Error('Failed to locate publishable Electron application'); + } - // const testPlatform = makeOptions.platform || process.platform as ForgePlatform; - if (!publishTargets) { - publishTargets = forgeConfig.publishers || []; - // .filter(publisher => (typeof publisher !== 'string' && publisher.platforms) - // ? publisher.platforms.includes(testPlatform) : true); - } - publishTargets = (publishTargets as ForgeConfigPublisher[]).map((target) => { - if (typeof target === 'string') { - return ( - (forgeConfig.publishers || []).find((p: ForgeConfigPublisher) => { - if (typeof p === 'string') return false; - if ((p as IForgePublisher).__isElectronForgePublisher) return false; - return (p as IForgeResolvablePublisher).name === target; - }) || { name: target } - ); - } - return target; - }); - - for (const publishTarget of publishTargets) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let publisher: PublisherBase; - if ((publishTarget as IForgePublisher).__isElectronForgePublisher) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - publisher = publishTarget as PublisherBase; - } else { - const resolvablePublishTarget = publishTarget as IForgeResolvablePublisher; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let PublisherClass: any; - await asyncOra(`Resolving publish target: ${chalk.cyan(resolvablePublishTarget.name)}`, async () => { - PublisherClass = requireSearch(dir, [resolvablePublishTarget.name]); - if (!PublisherClass) { - throw new Error( - `Could not find a publish target with the name: ${resolvablePublishTarget.name}. Make sure it's listed in the devDependencies of your package.json` + ctx.dir = resolvedDir; + ctx.forgeConfig = await getForgeConfig(resolvedDir); + }, + }, + { + title: 'Resolving publish targets', + task: async (ctx: PublishContext, task: ForgeListrTask) => { + const { dir, forgeConfig } = ctx; + + if (!publishTargets) { + publishTargets = forgeConfig.publishers || []; + } + publishTargets = (publishTargets as ForgeConfigPublisher[]).map((target) => { + if (typeof target === 'string') { + return ( + (forgeConfig.publishers || []).find((p: ForgeConfigPublisher) => { + if (typeof p === 'string') return false; + if ((p as IForgePublisher).__isElectronForgePublisher) return false; + return (p as IForgeResolvablePublisher).name === target; + }) || { name: target } + ); + } + return target; + }); + + ctx.publishers = []; + for (const publishTarget of publishTargets) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let publisher: PublisherBase; + if ((publishTarget as IForgePublisher).__isElectronForgePublisher) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + publisher = publishTarget as PublisherBase; + } else { + const resolvablePublishTarget = publishTarget as IForgeResolvablePublisher; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const PublisherClass: any = requireSearch(dir, [resolvablePublishTarget.name]); + if (!PublisherClass) { + throw new Error( + `Could not find a publish target with the name: ${resolvablePublishTarget.name}. Make sure it's listed in the devDependencies of your package.json` + ); + } + + publisher = new PublisherClass(resolvablePublishTarget.config || {}, resolvablePublishTarget.platforms); + } + + ctx.publishers.push(publisher); + } + + if (ctx.publishers.length) { + task.output = `Publishing to the following targets: ${chalk.magenta(`${ctx.publishers.map((publisher) => publisher.name).join(', ')}`)}`; + } + }, + options: { + persistentOutput: true, + }, + }, + { + title: dryRunResume ? 'Resuming from dry run...' : `Running ${chalk.yellow('make')} command`, + task: async (ctx, task) => { + const { dir, forgeConfig } = ctx; + const calculatedOutDir = outDir || getCurrentOutDir(dir, forgeConfig); + const dryRunDir = path.resolve(calculatedOutDir, 'publish-dry-run'); + + if (dryRunResume) { + d('attempting to resume from dry run'); + const publishes = await PublishState.loadFromDirectory(dryRunDir, dir); + task.title = `Resuming ${publishes.length} found dry runs...`; + + return task.newListr( + publishes.map((publishStates, index) => { + return { + title: `Publishing dry-run ${chalk.blue(`#${index + 1}`)}`, + task: async (ctx: PublishContext, task: ForgeListrTask) => { + const restoredMakeResults = publishStates.map(({ state }) => state); + d('restoring publish settings from dry run'); + + for (const makeResult of restoredMakeResults) { + for (const makePath of makeResult.artifacts) { + if (!(await fs.pathExists(makePath))) { + throw new Error(`Attempted to resume a dry run but an artifact (${makePath}) could not be found`); + } + } + } + + d('publishing for given state set'); + return task.newListr(publishDistributablesTasks, { + ctx: { + ...ctx, + makeResults: restoredMakeResults, + }, + rendererOptions: { + collapse: false, + collapseErrors: false, + }, + }); + }, + }; + }), + { + rendererOptions: { + collapse: false, + collapseErrors: false, + }, + } + ); + } + + d('triggering make'); + return listrMake( + { + dir, + interactive, + ...makeOptions, + }, + (results) => { + ctx.makeResults = results; + } ); - } - }); + }, + }, + ...(dryRunResume + ? [] + : dryRun + ? [ + { + title: 'Saving dry-run state', + task: async ({ dir, forgeConfig, makeResults }: PublishContext) => { + d('saving results of make in dry run state', makeResults); + const calculatedOutDir = outDir || getCurrentOutDir(dir, forgeConfig); + const dryRunDir = path.resolve(calculatedOutDir, 'publish-dry-run'); - publisher = new PublisherClass(resolvablePublishTarget.config || {}, resolvablePublishTarget.platforms); - } + await fs.remove(dryRunDir); + await PublishState.saveToDirectory(dryRunDir, makeResults!, dir); + }, + }, + ] + : publishDistributablesTasks), + ], + listrOptions + ); - await publisher.publish({ - dir, - makeResults, - forgeConfig, - }); - } + await runner.run(); }; export default publish; diff --git a/packages/api/core/src/api/start.ts b/packages/api/core/src/api/start.ts index eec98f4227..6c5482e8b5 100644 --- a/packages/api/core/src/api/start.ts +++ b/packages/api/core/src/api/start.ts @@ -1,14 +1,14 @@ import { spawn, SpawnOptions } from 'child_process'; import { getElectronVersion, listrCompatibleRebuildHook } from '@electron-forge/core-utils'; -import { ElectronProcess, ForgeArch, ForgePlatform, ResolvedForgeConfig, StartOptions } from '@electron-forge/shared-types'; +import { ElectronProcess, ForgeArch, ForgeListrTask, 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 getForgeConfig from '../util/forge-config'; -import { runHook } from '../util/hook'; +import { getHookListrTasks, runHook } from '../util/hook'; import { readMutatedPackageJson } from '../util/read-package-json'; import resolveDir from '../util/resolve-dir'; @@ -69,7 +69,7 @@ export default async ({ }, }, { - title: 'Rebuilding native modules', + title: 'Preparing native dependencies', task: async ({ dir, forgeConfig, packageJSON }, task) => { await listrCompatibleRebuildHook( dir, @@ -77,7 +77,7 @@ export default async ({ platform as ForgePlatform, arch as ForgeArch, forgeConfig.rebuildConfig, - task + task as ForgeListrTask ); }, options: { @@ -87,9 +87,9 @@ export default async ({ }, }, { - title: 'Generating assets', - task: async ({ forgeConfig }) => { - await runHook(forgeConfig, 'generateAssets', platform, arch); + title: `Running ${chalk.yellow('generateAssets')} hook`, + task: async ({ forgeConfig }, task) => { + return task.newListr(await getHookListrTasks(forgeConfig, 'generateAssets', platform, arch)); }, }, ], @@ -116,7 +116,7 @@ export default async ({ inspectBrk, }); if (typeof spawnedPluginChild === 'object' && 'tasks' in spawnedPluginChild) { - const innerRunner = new Listr([], listrOptions); + const innerRunner = new Listr([], listrOptions); for (const task of spawnedPluginChild.tasks) { innerRunner.add(task); } diff --git a/packages/api/core/src/util/hook.ts b/packages/api/core/src/util/hook.ts index d19449a185..1af87c8401 100644 --- a/packages/api/core/src/util/hook.ts +++ b/packages/api/core/src/util/hook.ts @@ -1,10 +1,12 @@ import { + ForgeListrTaskDefinition, ForgeMutatingHookFn, ForgeMutatingHookSignatures, ForgeSimpleHookFn, ForgeSimpleHookSignatures, ResolvedForgeConfig, } from '@electron-forge/shared-types'; +import chalk from 'chalk'; import debug from 'debug'; const d = debug('electron-forge:hook'); @@ -26,6 +28,29 @@ export const runHook = async ( await forgeConfig.pluginInterface.triggerHook(hookName, hookArgs); }; +export const getHookListrTasks = async ( + forgeConfig: ResolvedForgeConfig, + hookName: Hook, + ...hookArgs: ForgeSimpleHookSignatures[Hook] +): Promise => { + const { hooks } = forgeConfig; + const tasks: ForgeListrTaskDefinition[] = []; + if (hooks) { + d(`hook triggered: ${hookName}`); + if (typeof hooks[hookName] === 'function') { + d('calling hook:', hookName, 'with args:', hookArgs); + tasks.push({ + title: `Running ${chalk.yellow(hookName)} hook from forgeConfig`, + task: async () => { + await (hooks[hookName] as ForgeSimpleHookFn)(forgeConfig, ...hookArgs); + }, + }); + } + } + tasks.push(...(await forgeConfig.pluginInterface.getHookListrTasks(hookName, hookArgs))); + return tasks; +}; + export async function runMutatingHook( forgeConfig: ResolvedForgeConfig, hookName: Hook, diff --git a/packages/api/core/src/util/index.ts b/packages/api/core/src/util/index.ts index 4d3d3c030b..bf55a7b631 100644 --- a/packages/api/core/src/util/index.ts +++ b/packages/api/core/src/util/index.ts @@ -1,4 +1,4 @@ -import { getElectronVersion, hasYarn, packagerRebuildHook, yarnOrNpmSpawn } from '@electron-forge/core-utils'; +import { getElectronVersion, hasYarn, yarnOrNpmSpawn } from '@electron-forge/core-utils'; import { BuildIdentifierConfig, BuildIdentifierMap, fromBuildIdentifier } from './forge-config'; @@ -18,7 +18,5 @@ export default class ForgeUtils { hasYarn = hasYarn; - rebuildHook = packagerRebuildHook; - yarnOrNpmSpawn = yarnOrNpmSpawn; } diff --git a/packages/api/core/src/util/plugin-interface.ts b/packages/api/core/src/util/plugin-interface.ts index 8233026921..edd9bfeabe 100644 --- a/packages/api/core/src/util/plugin-interface.ts +++ b/packages/api/core/src/util/plugin-interface.ts @@ -1,5 +1,6 @@ import { PluginBase } from '@electron-forge/plugin-base'; import { + ForgeListrTaskDefinition, ForgeMutatingHookFn, ForgeMutatingHookSignatures, ForgeSimpleHookFn, @@ -69,12 +70,49 @@ export default class PluginInterface implements IForgePluginInterface { async triggerHook(hookName: Hook, hookArgs: ForgeSimpleHookSignatures[Hook]): Promise { for (const plugin of this.plugins) { if (typeof plugin.getHooks === 'function') { - const hook = plugin.getHooks()[hookName] as ForgeSimpleHookFn; - if (hook) await hook(this.config, ...hookArgs); + let hooks = plugin.getHooks()[hookName] as ForgeSimpleHookFn[] | ForgeSimpleHookFn; + if (hooks) { + if (typeof hooks === 'function') hooks = [hooks]; + for (const hook of hooks) { + await hook(this.config, ...hookArgs); + } + } } } } + async getHookListrTasks( + hookName: Hook, + hookArgs: ForgeSimpleHookSignatures[Hook] + ): Promise { + const tasks: ForgeListrTaskDefinition[] = []; + + for (const plugin of this.plugins) { + if (typeof plugin.getHooks === 'function') { + let hooks = plugin.getHooks()[hookName] as ForgeSimpleHookFn[] | ForgeSimpleHookFn; + if (hooks) { + if (typeof hooks === 'function') hooks = [hooks]; + for (const hook of hooks) { + tasks.push({ + title: `${chalk.cyan(`[plugin-${plugin.name}]`)} ${(hook as any).__hookName || `Running ${chalk.yellow(hookName)} hook`}`, + task: async (_, task) => { + if ((hook as any).__hookName) { + // Also give it the task + await (hook as any).call(task, ...(hookArgs as any[])); + } else { + await hook(this.config, ...hookArgs); + } + }, + options: {}, + }); + } + } + } + } + + return tasks; + } + async triggerMutatingHook( hookName: Hook, ...item: ForgeMutatingHookSignatures[Hook] @@ -82,9 +120,12 @@ export default class PluginInterface implements IForgePluginInterface { let result: ForgeMutatingHookSignatures[Hook][0] = item[0]; for (const plugin of this.plugins) { if (typeof plugin.getHooks === 'function') { - const hook = plugin.getHooks()[hookName] as ForgeMutatingHookFn; - if (hook) { - result = (await hook(this.config, ...item)) || result; + let hooks = plugin.getHooks()[hookName] as ForgeMutatingHookFn[] | ForgeMutatingHookFn; + if (hooks) { + if (typeof hooks === 'function') hooks = [hooks]; + for (const hook of hooks) { + result = (await hook(this.config, ...item)) || result; + } } } } diff --git a/packages/api/core/test/fast/publish_spec.ts b/packages/api/core/test/fast/publish_spec.ts index 8fc2e9deaf..d4c471c2e1 100644 --- a/packages/api/core/test/fast/publish_spec.ts +++ b/packages/api/core/test/fast/publish_spec.ts @@ -45,7 +45,11 @@ describe('publish', () => { publish = proxyquire.noCallThru().load('../../src/api/publish', { // eslint-disable-next-line @typescript-eslint/no-explicit-any - './make': async (...args: any[]) => makeStub(...args), + './make': { + listrMake: async (...args: any[]) => { + makeStub(...args); + }, + }, '../util/resolve-dir': async (dir: string) => resolveStub(dir), '../util/read-package-json': { readMutatedPackageJson: () => Promise.resolve(require('../fixture/dummy_app/package.json')), @@ -75,10 +79,9 @@ describe('publish', () => { publisherSpy.returns(Promise.resolve()); resolveStub.returns(path.resolve(__dirname, '../fixture/dummy_app')); - makeStub.returns([]); }); - it('should should call make with makeOptions', async () => { + it('should should call make', async () => { await publish({ dir: __dirname, interactive: false, @@ -109,7 +112,7 @@ describe('publish', () => { }); it('should call the resolved publisher with the appropriate args', async () => { - makeStub.returns([{ artifacts: ['artifact1', 'artifact2'] }]); + makeStub.onCall(0).callsArgWith(1, [{ artifacts: ['artifact1', 'artifact2'] }]); await publish({ dir: __dirname, interactive: false, @@ -117,6 +120,7 @@ describe('publish', () => { expect(publisherSpy.callCount).to.equal(1); // pluginInterface will be a new instance so we ignore it delete publisherSpy.firstCall.args[0].forgeConfig.pluginInterface; + delete publisherSpy.firstCall.args[0].setStatusLine; const testConfig = await loadFixtureConfig(); testConfig.publishers = publishers; @@ -132,7 +136,7 @@ describe('publish', () => { }); it('should call the provided publisher with the appropriate args', async () => { - makeStub.returns([{ artifacts: ['artifact1', 'artifact2'] }]); + makeStub.onCall(0).callsArgWith(1, [{ artifacts: ['artifact1', 'artifact2'] }]); await publish({ dir: __dirname, interactive: false, @@ -148,6 +152,7 @@ describe('publish', () => { expect(publisherSpy.callCount).to.equal(1); // pluginInterface will be a new instance so we ignore it delete publisherSpy.firstCall.args[0].forgeConfig.pluginInterface; + delete publisherSpy.firstCall.args[0].setStatusLine; const testConfig = await loadFixtureConfig(); testConfig.publishers = publishers; @@ -223,9 +228,15 @@ describe('publish', () => { dir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-forge-test-')); }); + beforeEach(() => { + resolveStub.returns(dir); + }); + describe('when creating a dry run', () => { beforeEach(async () => { - makeStub.returns(fakeMake('darwin')); + makeStub.onCall(0).callsArgWith(1, fakeMake('darwin')); + makeStub.onCall(1).callsArgWith(1, fakeMake('win32')); + const dryPath = path.resolve(dir, 'out', 'publish-dry-run'); await fs.mkdirs(dryPath); await fs.writeFile(path.resolve(dryPath, 'hash.json'), 'test'); @@ -237,7 +248,6 @@ describe('publish', () => { expect(await fs.pathExists(path.resolve(dryPath, 'hash.json'))).to.equal(false, 'previous hashes should be erased'); const backupDir = path.resolve(dir, 'out', 'backup'); await fs.move(dryPath, backupDir); - makeStub.returns(fakeMake('win32')); await publish({ dir, interactive: false, @@ -295,7 +305,7 @@ describe('publish', () => { const darwinIndex = publisherSpy.firstCall.args[0].makeResults[0].artifacts.some((a: string) => a.includes('darwin')) ? 0 : 1; const win32Index = darwinIndex === 0 ? 1 : 0; const darwinArgs = publisherSpy.getCall(darwinIndex).args[0]; - const darwinArtifacts = []; + const darwinArtifacts: unknown[] = []; for (const result of darwinArgs.makeResults) { darwinArtifacts.push(...result.artifacts); } @@ -305,7 +315,7 @@ describe('publish', () => { .sort() ); const win32Args = publisherSpy.getCall(win32Index).args[0]; - const win32Artifacts = []; + const win32Artifacts: unknown[] = []; for (const result of win32Args.makeResults) { win32Artifacts.push(...result.artifacts); } diff --git a/packages/api/core/test/fast/read-package-json_spec.ts b/packages/api/core/test/fast/read-package-json_spec.ts index d0a50d0a9e..50ae2f1fe1 100644 --- a/packages/api/core/test/fast/read-package-json_spec.ts +++ b/packages/api/core/test/fast/read-package-json_spec.ts @@ -30,6 +30,7 @@ describe('read-package-json', () => { triggerMutatingHook: (_hookName: string, pj: any) => Promise.resolve(pj), triggerHook: () => Promise.resolve(), overrideStartLogic: () => Promise.resolve(false), + getHookListrTasks: () => Promise.resolve([]), }, } as ResolvedForgeConfig) ).to.deep.equal(require('../../package.json')); @@ -43,6 +44,7 @@ describe('read-package-json', () => { triggerMutatingHook: () => Promise.resolve('test_mut'), triggerHook: () => Promise.resolve(), overrideStartLogic: () => Promise.resolve(false), + getHookListrTasks: () => Promise.resolve([]), }, } as ResolvedForgeConfig) ).to.deep.equal('test_mut'); diff --git a/packages/api/core/test/fast/start_spec.ts b/packages/api/core/test/fast/start_spec.ts index f90fc6ddd4..1fd42cfd12 100644 --- a/packages/api/core/test/fast/start_spec.ts +++ b/packages/api/core/test/fast/start_spec.ts @@ -29,6 +29,7 @@ describe('start', () => { pluginInterface: { overrideStartLogic: async () => shouldOverride, triggerHook: async () => false, + getHookListrTasks: () => Promise.resolve([]), }, }), '../util/resolve-dir': async (dir: string) => resolveStub(dir), diff --git a/packages/plugin/base/src/Plugin.ts b/packages/plugin/base/src/Plugin.ts index f9479f1b95..fc6bb43bcc 100644 --- a/packages/plugin/base/src/Plugin.ts +++ b/packages/plugin/base/src/Plugin.ts @@ -1,4 +1,13 @@ -import { ForgeHookMap, IForgePlugin, ResolvedForgeConfig, StartOptions, StartResult } from '@electron-forge/shared-types'; +import { + ForgeHookFn, + ForgeHookName, + ForgeListrTask, + ForgeMultiHookMap, + IForgePlugin, + ResolvedForgeConfig, + StartOptions, + StartResult, +} from '@electron-forge/shared-types'; export { StartOptions }; @@ -8,7 +17,7 @@ export default abstract class Plugin implements IForgePlugin { /** @internal */ __isElectronForgePlugin!: true; /** @internal */ - _resolvedHooks: ForgeHookMap = {}; + _resolvedHooks: ForgeMultiHookMap = {}; constructor(public config: C) { Object.defineProperty(this, '__isElectronForgePlugin', { @@ -25,7 +34,7 @@ export default abstract class Plugin implements IForgePlugin { this.getHooks = () => this._resolvedHooks; } - getHooks(): ForgeHookMap { + getHooks(): ForgeMultiHookMap { return {}; } @@ -34,4 +43,25 @@ export default abstract class Plugin implements IForgePlugin { } } +/* eslint-disable @typescript-eslint/no-explicit-any */ +// This is a filthy hack around typescript to allow internal hooks in our +// internal plugins to have some level of access to the "Task" that listr runs. +// Specifically the ability to set a custom task name and receive the task +// instance as a parameter +// +// This method is not type safe internally, but is type safe for consumers +// @internal +export const namedHookWithTaskFn = ( + hookFn: (task: ForgeListrTask | null, ...args: Parameters>) => ReturnType>, + name: string +): ForgeHookFn => { + function namedHookWithTaskInner(this: ForgeListrTask | null, ...args: any[]) { + return (hookFn as any)(this, ...args); + } + const fn = namedHookWithTaskInner as any; + fn.__hookName = name; + return fn; +}; +/* eslint-enable @typescript-eslint/no-explicit-any */ + export { Plugin as PluginBase }; diff --git a/packages/plugin/compile/package.json b/packages/plugin/compile/package.json index 913ea075a9..3463c32353 100644 --- a/packages/plugin/compile/package.json +++ b/packages/plugin/compile/package.json @@ -15,7 +15,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/plugin-base": "6.0.0", "@electron-forge/shared-types": "6.0.0", "fs-extra": "^10.0.0" diff --git a/packages/plugin/compile/src/lib/compile-hook.ts b/packages/plugin/compile/src/lib/compile-hook.ts index 3ba51fd901..88265502e2 100644 --- a/packages/plugin/compile/src/lib/compile-hook.ts +++ b/packages/plugin/compile/src/lib/compile-hook.ts @@ -1,46 +1,43 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; import { ForgeHookFn } from '@electron-forge/shared-types'; import fs from 'fs-extra'; export const createCompileHook = (originalDir: string): ForgeHookFn<'packageAfterCopy'> => async (_config, buildPath): Promise => { - await asyncOra('Compiling Application', async () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const compileCLI = require(path.resolve(originalDir, 'node_modules/electron-compile/lib/cli.js')); - - async function compileAndShim(appDir: string) { - for (const entry of await fs.readdir(appDir)) { - if (!entry.match(/^(node_modules|bower_components)$/)) { - const fullPath = path.join(appDir, entry); - - if ((await fs.stat(fullPath)).isDirectory()) { - const { log } = console; - console.log = () => { - /* disable log function for electron-compile */ - }; - await compileCLI.main(appDir, [fullPath]); - console.log = log; - } + // eslint-disable-next-line @typescript-eslint/no-var-requires + const compileCLI = require(path.resolve(originalDir, 'node_modules/electron-compile/lib/cli.js')); + + async function compileAndShim(appDir: string) { + for (const entry of await fs.readdir(appDir)) { + if (!entry.match(/^(node_modules|bower_components)$/)) { + const fullPath = path.join(appDir, entry); + + if ((await fs.stat(fullPath)).isDirectory()) { + const { log } = console; + console.log = () => { + /* disable log function for electron-compile */ + }; + await compileCLI.main(appDir, [fullPath]); + console.log = log; } } + } - const packageJSON = await fs.readJson(path.resolve(appDir, 'package.json')); + const packageJSON = await fs.readJson(path.resolve(appDir, 'package.json')); - const index = packageJSON.main || 'index.js'; - packageJSON.originalMain = index; - packageJSON.main = 'es6-shim.js'; + const index = packageJSON.main || 'index.js'; + packageJSON.originalMain = index; + packageJSON.main = 'es6-shim.js'; - await fs.writeFile( - path.join(appDir, 'es6-shim.js'), - await fs.readFile(path.join(path.resolve(originalDir, 'node_modules/electron-compile/lib/es6-shim.js')), 'utf8') - ); + await fs.writeFile( + path.join(appDir, 'es6-shim.js'), + await fs.readFile(path.join(path.resolve(originalDir, 'node_modules/electron-compile/lib/es6-shim.js')), 'utf8') + ); - await fs.writeJson(path.join(appDir, 'package.json'), packageJSON, { spaces: 2 }); - } + await fs.writeJson(path.join(appDir, 'package.json'), packageJSON, { spaces: 2 }); + } - await compileAndShim(buildPath); - }); + await compileAndShim(buildPath); }; diff --git a/packages/plugin/webpack/package.json b/packages/plugin/webpack/package.json index 186efdae9e..82729a81ae 100644 --- a/packages/plugin/webpack/package.json +++ b/packages/plugin/webpack/package.json @@ -24,7 +24,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/core-utils": "6.0.0", "@electron-forge/plugin-base": "6.0.0", "@electron-forge/shared-types": "6.0.0", diff --git a/packages/plugin/webpack/src/WebpackPlugin.ts b/packages/plugin/webpack/src/WebpackPlugin.ts index c3ca8d9ba6..58d547ad20 100644 --- a/packages/plugin/webpack/src/WebpackPlugin.ts +++ b/packages/plugin/webpack/src/WebpackPlugin.ts @@ -1,10 +1,9 @@ import http from 'http'; import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; -import { getElectronVersion, packagerRebuildHook } from '@electron-forge/core-utils'; -import { PluginBase } from '@electron-forge/plugin-base'; -import { ForgeHookMap, ResolvedForgeConfig, StartResult } from '@electron-forge/shared-types'; +import { getElectronVersion, listrCompatibleRebuildHook } from '@electron-forge/core-utils'; +import { namedHookWithTaskFn, PluginBase } from '@electron-forge/plugin-base'; +import { ForgeMultiHookMap, 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,21 +148,31 @@ export default class WebpackPlugin extends PluginBase { return this._configGenerator; } - getHooks(): ForgeHookMap { + getHooks(): ForgeMultiHookMap { return { - prePackage: async (config, platform, arch) => { - this.isProd = true; - await fs.remove(this.baseDir); - await packagerRebuildHook( - this.projectDir, - await getElectronVersion(this.projectDir, await fs.readJson(path.join(this.projectDir, 'package.json'))), - platform, - arch, - config.rebuildConfig - ); - await this.compileMain(); - await this.compileRenderers(); - }, + prePackage: [ + namedHookWithTaskFn<'prePackage'>(async (task, config, platform, arch) => { + if (!task) { + throw new Error('Incompatible usage of webpack-plugin prePackage hook'); + } + + this.isProd = true; + await fs.remove(this.baseDir); + await listrCompatibleRebuildHook( + this.projectDir, + await getElectronVersion(this.projectDir, await fs.readJson(path.join(this.projectDir, 'package.json'))), + platform, + arch, + config.rebuildConfig, + task, + chalk.cyan('[plugin-webpack] ') + ); + }, 'Preparing native dependencies'), + namedHookWithTaskFn<'prePackage'>(async () => { + await this.compileMain(); + await this.compileRenderers(); + }, 'Building webpack bundles'), + ], postStart: async (_config, child) => { d('hooking electron process exit'); child.on('exit', () => { @@ -268,25 +277,21 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`); }; compileRenderers = async (watch = false): Promise => { - await asyncOra('Compiling Renderer Template', async () => { - const stats = await this.runWebpack(await this.configGenerator.getRendererConfig(this.config.renderer.entryPoints), true); - if (!watch && stats?.hasErrors()) { - throw new Error(`Compilation errors in the renderer: ${stats.toString()}`); - } - }); + const stats = await this.runWebpack(await this.configGenerator.getRendererConfig(this.config.renderer.entryPoints), true); + if (!watch && stats?.hasErrors()) { + throw new Error(`Compilation errors in the renderer: ${stats.toString()}`); + } for (const entryPoint of this.config.renderer.entryPoints) { if ((isLocalWindow(entryPoint) && !!entryPoint.preload) || isPreloadOnly(entryPoint)) { - await asyncOra(`Compiling Renderer Preload: ${chalk.cyan(entryPoint.name)}`, async () => { - const stats = await this.runWebpack( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - [await this.configGenerator.getPreloadConfigForEntryPoint(entryPoint)] - ); + const stats = await this.runWebpack( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + [await this.configGenerator.getPreloadConfigForEntryPoint(entryPoint)] + ); - if (stats?.hasErrors()) { - throw new Error(`Compilation errors in the preload (${entryPoint.name}): ${stats.toString()}`); - } - }); + if (stats?.hasErrors()) { + throw new Error(`Compilation errors in the preload (${entryPoint.name}): ${stats.toString()}`); + } } } }; diff --git a/packages/publisher/base/src/Publisher.ts b/packages/publisher/base/src/Publisher.ts index ffae2cb3cb..cb00c65194 100644 --- a/packages/publisher/base/src/Publisher.ts +++ b/packages/publisher/base/src/Publisher.ts @@ -1,4 +1,4 @@ -import { ForgeMakeResult, ForgePlatform, IForgePublisher, ResolvedForgeConfig } from '@electron-forge/shared-types'; +import { ForgeListrTaskDefinition, ForgeMakeResult, ForgePlatform, IForgePublisher, ResolvedForgeConfig } from '@electron-forge/shared-types'; export interface PublisherOptions { /** @@ -15,6 +15,12 @@ export interface PublisherOptions { * You probably shouldn't use this */ forgeConfig: ResolvedForgeConfig; + /** + * A method that allows the publisher to provide status / progress updates + * to the user. This method currently maps to setting the "output" line + * in the publisher listr task. + */ + setStatusLine: (statusLine: string) => void; } export default abstract class Publisher implements IForgePublisher { @@ -56,7 +62,7 @@ export default abstract class Publisher implements IForgePublisher { * be appending files to the existing version. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - async publish(opts: PublisherOptions): Promise { + async publish(opts: PublisherOptions): Promise { throw new Error(`Publisher ${this.name} did not implement the publish method`); } } diff --git a/packages/publisher/bitbucket/package.json b/packages/publisher/bitbucket/package.json index e3b67a7424..391ebf39ed 100644 --- a/packages/publisher/bitbucket/package.json +++ b/packages/publisher/bitbucket/package.json @@ -15,7 +15,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/publisher-base": "6.0.0", "form-data": "^4.0.0", "fs-extra": "^10.0.0", diff --git a/packages/publisher/bitbucket/src/PublisherBitbucket.ts b/packages/publisher/bitbucket/src/PublisherBitbucket.ts index 223e0bb1c4..61d3ba88a8 100644 --- a/packages/publisher/bitbucket/src/PublisherBitbucket.ts +++ b/packages/publisher/bitbucket/src/PublisherBitbucket.ts @@ -1,6 +1,5 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; import { PublisherBase, PublisherOptions } from '@electron-forge/publisher-base'; import FormData from 'form-data'; import fs from 'fs-extra'; @@ -11,7 +10,7 @@ import { PublisherBitbucketConfig } from './Config'; export default class PublisherBitbucket extends PublisherBase { name = 'bitbucket'; - async publish({ makeResults }: PublisherOptions): Promise { + async publish({ makeResults, setStatusLine }: PublisherOptions): Promise { const { config } = this; const hasRepositoryConfig = config.repository && typeof config.repository; const replaceExistingFiles = Boolean(config.replaceExistingFiles); @@ -43,43 +42,40 @@ export default class PublisherBitbucket extends PublisherBase { - for (const artifactPath of makeResult.artifacts) { - const fileName = path.basename(artifactPath); + for (const artifactPath of makeResult.artifacts) { + const fileName = path.basename(artifactPath); - const response = await fetch(`${apiUrl}/${fileName}`, { - headers: { - Authorization: `Basic ${encodedUserAndPass}`, - }, - method: 'HEAD', - // We set redirect to 'manual' so that we get the 302 redirects if the file - // already exists - redirect: 'manual', - }); + const response = await fetch(`${apiUrl}/${fileName}`, { + headers: { + Authorization: `Basic ${encodedUserAndPass}`, + }, + method: 'HEAD', + // We set redirect to 'manual' so that we get the 302 redirects if the file + // already exists + redirect: 'manual', + }); - if (response.status === 302) { - throw new Error( - `Unable to publish "${fileName}" as it has been published previously. Use the "replaceExistingFiles" property in your Forge config to override this.` - ); - } + if (response.status === 302) { + throw new Error( + `Unable to publish "${fileName}" as it has been published previously. Use the "replaceExistingFiles" property in your Forge config to override this.` + ); } - }); + } } - await asyncOra(`Uploading result (${index + 1}/${makeResults.length})`, async () => { - const response = await fetch(apiUrl, { - headers: { - Authorization: `Basic ${encodedUserAndPass}`, - }, - method: 'POST', - body: data, - }); - - // We will get a 200 on the inital upload and a 201 if publishing over the same version - if (response.status !== 200 && response.status !== 201) { - throw new Error(`Unexpected response code from Bitbucket: ${response.status} ${response.statusText}\n\nBody:\n${await response.text()}`); - } + setStatusLine(`Uploading distributable (${index + 1}/${makeResults.length})`); + const response = await fetch(apiUrl, { + headers: { + Authorization: `Basic ${encodedUserAndPass}`, + }, + method: 'POST', + body: data, }); + + // We will get a 200 on the inital upload and a 201 if publishing over the same version + if (response.status !== 200 && response.status !== 201) { + throw new Error(`Unexpected response code from Bitbucket: ${response.status} ${response.statusText}\n\nBody:\n${await response.text()}`); + } } } } diff --git a/packages/publisher/electron-release-server/package.json b/packages/publisher/electron-release-server/package.json index 70ac67cf6a..c7d56c3138 100644 --- a/packages/publisher/electron-release-server/package.json +++ b/packages/publisher/electron-release-server/package.json @@ -18,7 +18,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/publisher-base": "6.0.0", "@electron-forge/shared-types": "6.0.0", "debug": "^4.3.1", diff --git a/packages/publisher/electron-release-server/src/PublisherERS.ts b/packages/publisher/electron-release-server/src/PublisherERS.ts index ec5cc7af78..3902cf9d97 100644 --- a/packages/publisher/electron-release-server/src/PublisherERS.ts +++ b/packages/publisher/electron-release-server/src/PublisherERS.ts @@ -1,6 +1,5 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; import { PublisherBase, PublisherOptions } from '@electron-forge/publisher-base'; import { ForgeArch, ForgePlatform } from '@electron-forge/shared-types'; import debug from 'debug'; @@ -43,7 +42,7 @@ export const ersPlatform = (platform: ForgePlatform, arch: ForgeArch): string => export default class PublisherERS extends PublisherBase { name = 'electron-release-server'; - async publish({ makeResults }: PublisherOptions): Promise { + async publish({ makeResults, setStatusLine }: PublisherOptions): Promise { const { config } = this; if (!(config.baseUrl && config.username && config.password)) { @@ -110,48 +109,43 @@ export default class PublisherERS extends PublisherBase { } let uploaded = 0; - const getText = () => `Uploading Artifacts ${uploaded}/${artifacts.length}`; - - await asyncOra(getText(), async (uploadSpinner) => { - const updateSpinner = () => { - uploadSpinner.text = getText(); - }; - - await Promise.all( - artifacts.map(async (artifactPath) => { - if (existingVersion) { - const existingAsset = existingVersion.assets.find((asset) => asset.name === path.basename(artifactPath)); - - if (existingAsset) { - d('asset at path:', artifactPath, 'already exists on server'); - uploaded += 1; - updateSpinner(); - return; - } + const updateStatusLine = () => setStatusLine(`Uploading distributable (${uploaded}/${artifacts.length})`); + updateStatusLine(); + + await Promise.all( + artifacts.map(async (artifactPath) => { + if (existingVersion) { + const existingAsset = existingVersion.assets.find((asset) => asset.name === path.basename(artifactPath)); + + if (existingAsset) { + d('asset at path:', artifactPath, 'already exists on server'); + uploaded += 1; + updateStatusLine(); + return; } - d('attempting to upload asset:', artifactPath); - const artifactForm = new FormData(); - artifactForm.append('token', token); - artifactForm.append('version', packageJSON.version); - artifactForm.append('platform', ersPlatform(makeResult.platform, makeResult.arch)); - - // see https://github.com/form-data/form-data/issues/426 - const fileOptions = { - knownLength: fs.statSync(artifactPath).size, - }; - artifactForm.append('file', fs.createReadStream(artifactPath), fileOptions); - - await authFetch('api/asset', { - method: 'POST', - body: artifactForm, - headers: artifactForm.getHeaders(), - }); - d('upload successful for asset:', artifactPath); - uploaded += 1; - updateSpinner(); - }) - ); - }); + } + d('attempting to upload asset:', artifactPath); + const artifactForm = new FormData(); + artifactForm.append('token', token); + artifactForm.append('version', packageJSON.version); + artifactForm.append('platform', ersPlatform(makeResult.platform, makeResult.arch)); + + // see https://github.com/form-data/form-data/issues/426 + const fileOptions = { + knownLength: fs.statSync(artifactPath).size, + }; + artifactForm.append('file', fs.createReadStream(artifactPath), fileOptions); + + await authFetch('api/asset', { + method: 'POST', + body: artifactForm, + headers: artifactForm.getHeaders(), + }); + d('upload successful for asset:', artifactPath); + uploaded += 1; + updateStatusLine(); + }) + ); } } } diff --git a/packages/publisher/electron-release-server/test/PublisherERS_spec.ts b/packages/publisher/electron-release-server/test/PublisherERS_spec.ts index 53e2f21e4d..ccc36d3f5e 100644 --- a/packages/publisher/electron-release-server/test/PublisherERS_spec.ts +++ b/packages/publisher/electron-release-server/test/PublisherERS_spec.ts @@ -4,10 +4,14 @@ import fetchMock from 'fetch-mock'; import proxyquire from 'proxyquire'; import { stub } from 'sinon'; +import type { PublisherERS as PublisherERSType } from '../src/PublisherERS'; + +const noop = () => void 0; + describe('PublisherERS', () => { let fetch: typeof fetchMock; // eslint-disable-next-line @typescript-eslint/no-explicit-any - let PublisherERS: any; + let PublisherERS: typeof PublisherERSType; beforeEach(() => { fetch = fetchMock.sandbox(); @@ -55,7 +59,7 @@ describe('PublisherERS', () => { }, ]; - await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig }); + await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig, setStatusLine: noop }); const calls = fetch.calls(); @@ -102,7 +106,7 @@ describe('PublisherERS', () => { }, ]; - await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig }); + await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig, setStatusLine: noop }); const calls = fetch.calls(); @@ -139,7 +143,7 @@ describe('PublisherERS', () => { }, ]; - await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig }); + await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig, setStatusLine: noop }); const calls = fetch.calls(); expect(calls).to.have.length(2); @@ -178,7 +182,7 @@ describe('PublisherERS', () => { }, ]; - await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig }); + await publisher.publish({ makeResults, dir: '', forgeConfig: {} as ResolvedForgeConfig, setStatusLine: noop }); const calls = fetch.calls(); @@ -196,9 +200,10 @@ describe('PublisherERS', () => { }); it('fails if username and password are not provided', () => { + // @ts-expect-error testing invalid options const publisher = new PublisherERS({}); - expect(publisher.publish({ makeResults: [], dir: '', forgeConfig: {} as ResolvedForgeConfig })).to.eventually.be.rejectedWith( + expect(publisher.publish({ makeResults: [], dir: '', forgeConfig: {} as ResolvedForgeConfig, setStatusLine: noop })).to.eventually.be.rejectedWith( 'In order to publish to ERS you must set the "electronReleaseServer.baseUrl", "electronReleaseServer.username" and "electronReleaseServer.password" properties in your Forge config. See the docs for more info' ); }); @@ -211,7 +216,7 @@ describe('PublisherERS', () => { username: 'test', password: 'test', }); - return expect(publisher.publish({ makeResults: [], dir: '', forgeConfig: {} as ResolvedForgeConfig })).to.eventually.be.rejectedWith( + return expect(publisher.publish({ makeResults: [], dir: '', forgeConfig: {} as ResolvedForgeConfig, setStatusLine: noop })).to.eventually.be.rejectedWith( 'ERS publish failed with status code: 400 (http://example.com/api/auth/login)' ); }); diff --git a/packages/publisher/github/package.json b/packages/publisher/github/package.json index 4398a6d13d..8479e665d9 100644 --- a/packages/publisher/github/package.json +++ b/packages/publisher/github/package.json @@ -21,7 +21,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/publisher-base": "6.0.0", "@electron-forge/shared-types": "6.0.0", "@octokit/core": "^3.2.4", diff --git a/packages/publisher/github/src/PublisherGithub.ts b/packages/publisher/github/src/PublisherGithub.ts index a84e965309..199d888b82 100644 --- a/packages/publisher/github/src/PublisherGithub.ts +++ b/packages/publisher/github/src/PublisherGithub.ts @@ -1,6 +1,5 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; import { PublisherBase, PublisherOptions } from '@electron-forge/publisher-base'; import { ForgeMakeResult } from '@electron-forge/shared-types'; import { GetResponseDataTypeFromEndpointMethod } from '@octokit/types'; @@ -22,7 +21,7 @@ interface GitHubRelease { export default class PublisherGithub extends PublisherBase { name = 'github'; - async publish({ makeResults }: PublisherOptions): Promise { + async publish({ makeResults, setStatusLine }: PublisherOptions): Promise { const { config } = this; const perReleaseArtifacts: { @@ -54,79 +53,77 @@ export default class PublisherGithub extends PublisherBase { - try { + setStatusLine(`Searching for target release: ${releaseName}`); + try { + release = ( + await github.getGitHub().repos.listReleases({ + owner: config.repository.owner, + repo: config.repository.name, + per_page: 100, + }) + ).data.find((testRelease: GitHubRelease) => testRelease.tag_name === releaseName); + if (!release) { + throw new NoReleaseError(404); + } + } catch (err) { + if (err instanceof NoReleaseError && err.code === 404) { + // Release does not exist, let's make it release = ( - await github.getGitHub().repos.listReleases({ + await github.getGitHub().repos.createRelease({ owner: config.repository.owner, repo: config.repository.name, - per_page: 100, + tag_name: releaseName, + name: releaseName, + draft: config.draft !== false, + prerelease: config.prerelease === true, }) - ).data.find((testRelease: GitHubRelease) => testRelease.tag_name === releaseName); - if (!release) { - throw new NoReleaseError(404); - } - } catch (err) { - if (err instanceof NoReleaseError && err.code === 404) { - // Release does not exist, let's make it - release = ( - await github.getGitHub().repos.createRelease({ - owner: config.repository.owner, - repo: config.repository.name, - tag_name: releaseName, - name: releaseName, - draft: config.draft !== false, - prerelease: config.prerelease === true, - }) - ).data; - } else { - // Unknown error - throw err; - } + ).data; + } else { + // Unknown error + throw err; } - }); + } let uploaded = 0; - await asyncOra(`Uploading Artifacts ${uploaded}/${artifacts.length} to ${releaseName}`, async (uploadSpinner) => { - const updateSpinner = () => { - uploadSpinner.text = `Uploading Artifacts ${uploaded}/${artifacts.length} to ${releaseName}`; - }; - - const flatArtifacts: string[] = []; - for (const artifact of artifacts) { - flatArtifacts.push(...artifact.artifacts); - } + const updateUploadStatus = () => { + setStatusLine(`Uploading distributable (${uploaded}/${artifacts.length} to ${releaseName})`); + }; + updateUploadStatus(); + + const flatArtifacts: string[] = []; + for (const artifact of artifacts) { + flatArtifacts.push(...artifact.artifacts); + } - await Promise.all( - flatArtifacts.map(async (artifactPath) => { - const done = () => { - uploaded += 1; - updateSpinner(); - }; - const artifactName = path.basename(artifactPath); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (release!.assets.find((asset: OctokitReleaseAsset) => asset.name === artifactName)) { - return done(); - } - await github.getGitHub().repos.uploadReleaseAsset({ - owner: config.repository.owner, - repo: config.repository.name, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - release_id: release!.id, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - url: release!.upload_url, - // https://github.com/octokit/rest.js/issues/1645 - data: (await fs.readFile(artifactPath)) as unknown as string, - headers: { - 'content-type': mime.lookup(artifactPath) || 'application/octet-stream', - 'content-length': (await fs.stat(artifactPath)).size, - }, - name: path.basename(artifactPath), - }); + await Promise.all( + flatArtifacts.map(async (artifactPath) => { + const done = () => { + uploaded += 1; + updateUploadStatus(); + }; + const artifactName = path.basename(artifactPath); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (release!.assets.find((asset: OctokitReleaseAsset) => asset.name === artifactName)) { return done(); - }) - ); - }); + } + await github.getGitHub().repos.uploadReleaseAsset({ + owner: config.repository.owner, + repo: config.repository.name, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + release_id: release!.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + url: release!.upload_url, + // https://github.com/octokit/rest.js/issues/1645 + data: (await fs.readFile(artifactPath)) as unknown as string, + headers: { + 'content-type': mime.lookup(artifactPath) || 'application/octet-stream', + 'content-length': (await fs.stat(artifactPath)).size, + }, + name: path.basename(artifactPath), + }); + return done(); + }) + ); } } } diff --git a/packages/publisher/nucleus/package.json b/packages/publisher/nucleus/package.json index 95603fa8ec..99f0c7d91f 100644 --- a/packages/publisher/nucleus/package.json +++ b/packages/publisher/nucleus/package.json @@ -15,7 +15,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/publisher-base": "6.0.0", "@electron-forge/shared-types": "6.0.0", "debug": "^4.3.1", diff --git a/packages/publisher/nucleus/src/PublisherNucleus.ts b/packages/publisher/nucleus/src/PublisherNucleus.ts index d8d074f04c..e0107884ad 100644 --- a/packages/publisher/nucleus/src/PublisherNucleus.ts +++ b/packages/publisher/nucleus/src/PublisherNucleus.ts @@ -1,7 +1,6 @@ import fs from 'fs'; import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; import { PublisherBase, PublisherOptions } from '@electron-forge/publisher-base'; import debug from 'debug'; import FormData from 'form-data'; @@ -29,41 +28,40 @@ export default class PublisherNucleus extends PublisherBase { + async publish({ makeResults, setStatusLine }: PublisherOptions): Promise { const { config } = this; const collapsedResults = this.collapseMakeResults(makeResults); for (const [resultIdx, makeResult] of collapsedResults.entries()) { - const msg = `Uploading result (${resultIdx + 1}/${collapsedResults.length})`; + const msg = `Uploading distributable (${resultIdx + 1}/${collapsedResults.length})`; d(msg); - await asyncOra(msg, async () => { - const data = new FormData(); - data.append('platform', makeResult.platform); - data.append('arch', makeResult.arch); - data.append('version', makeResult.packageJSON.version); + setStatusLine(msg); + const data = new FormData(); + data.append('platform', makeResult.platform); + data.append('arch', makeResult.arch); + data.append('version', makeResult.packageJSON.version); - let artifactIdx = 0; - for (const artifactPath of makeResult.artifacts) { - // Skip the RELEASES file, it is automatically generated on the server - if (path.basename(artifactPath).toLowerCase() === 'releases') continue; - data.append(`file${artifactIdx}`, fs.createReadStream(artifactPath)); - artifactIdx += 1; - } - - const response = await fetch(`${config.host}/rest/app/${config.appId}/channel/${config.channelId}/upload`, { - headers: { - Authorization: config.token, - }, - method: 'POST', - body: data, - }); + let artifactIdx = 0; + for (const artifactPath of makeResult.artifacts) { + // Skip the RELEASES file, it is automatically generated on the server + if (path.basename(artifactPath).toLowerCase() === 'releases') continue; + data.append(`file${artifactIdx}`, fs.createReadStream(artifactPath)); + artifactIdx += 1; + } - if (response.status !== 200) { - throw new Error(`Unexpected response code from Nucleus: ${response.status}\n\nBody:\n${await response.text()}`); - } + const response = await fetch(`${config.host}/rest/app/${config.appId}/channel/${config.channelId}/upload`, { + headers: { + Authorization: config.token, + }, + method: 'POST', + body: data, }); + + if (response.status !== 200) { + throw new Error(`Unexpected response code from Nucleus: ${response.status}\n\nBody:\n${await response.text()}`); + } } } } diff --git a/packages/publisher/s3/package.json b/packages/publisher/s3/package.json index d63b78bfcd..76a534d31d 100644 --- a/packages/publisher/s3/package.json +++ b/packages/publisher/s3/package.json @@ -19,7 +19,6 @@ "@aws-sdk/client-s3": "^3.28.0", "@aws-sdk/lib-storage": "^3.28.0", "@aws-sdk/types": "^3.25.0", - "@electron-forge/async-ora": "6.0.0", "@electron-forge/publisher-base": "6.0.0", "@electron-forge/shared-types": "6.0.0", "debug": "^4.3.1" diff --git a/packages/publisher/s3/src/PublisherS3.ts b/packages/publisher/s3/src/PublisherS3.ts index d234215e62..8471452f1b 100644 --- a/packages/publisher/s3/src/PublisherS3.ts +++ b/packages/publisher/s3/src/PublisherS3.ts @@ -4,7 +4,6 @@ import path from 'path'; import { S3Client } from '@aws-sdk/client-s3'; import { Progress, Upload } from '@aws-sdk/lib-storage'; import { Credentials } from '@aws-sdk/types'; -import { asyncOra } from '@electron-forge/async-ora'; import { PublisherBase, PublisherOptions } from '@electron-forge/publisher-base'; import debug from 'debug'; @@ -22,7 +21,7 @@ type S3Artifact = { export default class PublisherS3 extends PublisherBase { name = 's3'; - async publish({ makeResults }: PublisherOptions): Promise { + async publish({ makeResults, setStatusLine }: PublisherOptions): Promise { const artifacts: S3Artifact[] = []; if (!this.config.bucket) { @@ -50,35 +49,34 @@ export default class PublisherS3 extends PublisherBase { d('creating s3 client with options:', this.config); let uploaded = 0; - const spinnerText = () => `Uploading Artifacts ${uploaded}/${artifacts.length}`; - - await asyncOra(spinnerText(), async (uploadSpinner) => { - await Promise.all( - artifacts.map(async (artifact) => { - d('uploading:', artifact.path); - const uploader = new Upload({ - client: s3Client, - params: { - Body: fs.createReadStream(artifact.path), - Bucket: this.config.bucket, - Key: this.keyForArtifact(artifact), - ACL: this.config.public ? 'public-read' : 'private', - }, - }); - - uploader.on('httpUploadProgress', (progress: Progress) => { - if (progress.total) { - const percentage = `${Math.round(((progress.loaded || 0) / progress.total) * 100)}%`; - d(`Upload Progress (${path.basename(artifact.path)}) ${percentage}`); - } - }); - - await uploader.done(); - uploaded += 1; - uploadSpinner.text = spinnerText(); - }) - ); - }); + const updateStatusLine = () => setStatusLine(`Uploading distributable (${uploaded}/${artifacts.length})`); + + updateStatusLine(); + await Promise.all( + artifacts.map(async (artifact) => { + d('uploading:', artifact.path); + const uploader = new Upload({ + client: s3Client, + params: { + Body: fs.createReadStream(artifact.path), + Bucket: this.config.bucket, + Key: this.keyForArtifact(artifact), + ACL: this.config.public ? 'public-read' : 'private', + }, + }); + + uploader.on('httpUploadProgress', (progress: Progress) => { + if (progress.total) { + const percentage = `${Math.round(((progress.loaded || 0) / progress.total) * 100)}%`; + d(`Upload Progress (${path.basename(artifact.path)}) ${percentage}`); + } + }); + + await uploader.done(); + uploaded += 1; + updateStatusLine(); + }) + ); } keyForArtifact(artifact: S3Artifact): string { diff --git a/packages/publisher/snapcraft/package.json b/packages/publisher/snapcraft/package.json index 20114e345b..51f930ac6d 100644 --- a/packages/publisher/snapcraft/package.json +++ b/packages/publisher/snapcraft/package.json @@ -15,7 +15,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/publisher-base": "6.0.0", "fs-extra": "^10.0.0" }, diff --git a/packages/publisher/snapcraft/src/PublisherSnapcraft.ts b/packages/publisher/snapcraft/src/PublisherSnapcraft.ts index 929005cfa7..8ce42ea413 100644 --- a/packages/publisher/snapcraft/src/PublisherSnapcraft.ts +++ b/packages/publisher/snapcraft/src/PublisherSnapcraft.ts @@ -1,6 +1,5 @@ import path from 'path'; -import { asyncOra } from '@electron-forge/async-ora'; import { PublisherBase, PublisherOptions } from '@electron-forge/publisher-base'; import fs from 'fs-extra'; @@ -13,7 +12,7 @@ const Snapcraft = require('electron-installer-snap/src/snapcraft'); export default class PublisherSnapcraft extends PublisherBase { name = 'snapcraft'; - async publish({ dir, makeResults }: PublisherOptions): Promise { + async publish({ dir, makeResults, setStatusLine }: PublisherOptions): Promise { const artifacts = makeResults.reduce((flat, makeResult) => { flat.push(...makeResult.artifacts); return flat; @@ -34,10 +33,9 @@ export default class PublisherSnapcraft extends PublisherBase { - const snapcraft = new Snapcraft(); - await snapcraft.run(dir, 'push', this.config, snapArtifacts); - }); + setStatusLine('Pushing snap to the snap store'); + const snapcraft = new Snapcraft(); + await snapcraft.run(dir, 'push', this.config, snapArtifacts); } } diff --git a/packages/template/base/package.json b/packages/template/base/package.json index 6d7331d13c..c224126f08 100644 --- a/packages/template/base/package.json +++ b/packages/template/base/package.json @@ -14,7 +14,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/shared-types": "6.0.0", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", diff --git a/packages/template/webpack-typescript/package.json b/packages/template/webpack-typescript/package.json index e30fc66667..06795e8348 100644 --- a/packages/template/webpack-typescript/package.json +++ b/packages/template/webpack-typescript/package.json @@ -14,7 +14,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/shared-types": "6.0.0", "@electron-forge/template-base": "6.0.0", "fs-extra": "^10.0.0" diff --git a/packages/template/webpack/package.json b/packages/template/webpack/package.json index b10e410b06..6077cd4a10 100644 --- a/packages/template/webpack/package.json +++ b/packages/template/webpack/package.json @@ -14,7 +14,6 @@ "node": ">= 14.17.5" }, "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/shared-types": "6.0.0", "@electron-forge/template-base": "6.0.0", "fs-extra": "^10.0.0" diff --git a/packages/utils/core-utils/package.json b/packages/utils/core-utils/package.json index 9f577b1185..038abd0d35 100644 --- a/packages/utils/core-utils/package.json +++ b/packages/utils/core-utils/package.json @@ -8,7 +8,6 @@ "main": "dist/index.js", "typings": "dist/index.d.ts", "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron-forge/shared-types": "6.0.0", "@electron/rebuild": "^3.2.10", "@malept/cross-spawn-promise": "^2.0.0", diff --git a/packages/utils/core-utils/src/rebuild.ts b/packages/utils/core-utils/src/rebuild.ts index 029bec3a88..76cf0d528c 100644 --- a/packages/utils/core-utils/src/rebuild.ts +++ b/packages/utils/core-utils/src/rebuild.ts @@ -1,9 +1,8 @@ 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'; +import { RebuildOptions } from '@electron/rebuild'; export const listrCompatibleRebuildHook = async ( buildPath: string, @@ -11,9 +10,10 @@ export const listrCompatibleRebuildHook = async ( platform: ForgePlatform, arch: ForgeArch, config: Partial = {}, - task: ForgeListrTask + task: ForgeListrTask, + taskTitlePrefix = '' ): Promise => { - task.title = 'Preparing native dependencies'; + task.title = `${taskTitlePrefix}Preparing native dependencies`; const options: RebuildOptions = { ...config, @@ -26,12 +26,12 @@ export const listrCompatibleRebuildHook = async ( stdio: ['pipe', 'pipe', 'pipe', 'ipc'], }); - let pendingError: unknown; + let pendingError: Error; let found = 0; let done = 0; const redraw = () => { - task.title = `Preparing native dependencies: ${done} / ${found}`; + task.title = `${taskTitlePrefix}Preparing native dependencies: ${done} / ${found}`; }; child.stdout?.on('data', (chunk) => { @@ -41,7 +41,7 @@ export const listrCompatibleRebuildHook = async ( task.output = chunk.toString(); }); - child.on('message', (message: any) => { + child.on('message', (message: { msg: string; err: { message: string; stack: string } }) => { switch (message.msg) { case 'module-found': { found += 1; @@ -55,7 +55,7 @@ export const listrCompatibleRebuildHook = async ( } case 'rebuild-error': { pendingError = new Error(message.err.message); - (pendingError as any).stack = message.err.stack; + pendingError.stack = message.err.stack; break; } case 'rebuild-done': { @@ -75,39 +75,3 @@ export const listrCompatibleRebuildHook = async ( }); }); }; - -export const packagerRebuildHook = 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/utils/core-utils/src/remote-rebuild.ts b/packages/utils/core-utils/src/remote-rebuild.ts index ecac55b022..bf10955d35 100644 --- a/packages/utils/core-utils/src/remote-rebuild.ts +++ b/packages/utils/core-utils/src/remote-rebuild.ts @@ -10,17 +10,17 @@ 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.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' }); + process.send?.({ msg: 'rebuild-done' }); // eslint-disable-next-line no-process-exit return process.exit(0); }) .catch((err) => { - process.send!({ + process.send?.({ msg: 'rebuild-error', err: { message: err.message, diff --git a/packages/utils/types/package.json b/packages/utils/types/package.json index e0b0fefa9e..8ce15c454e 100644 --- a/packages/utils/types/package.json +++ b/packages/utils/types/package.json @@ -8,11 +8,9 @@ "main": "dist/index.js", "typings": "dist/index.d.ts", "dependencies": { - "@electron-forge/async-ora": "6.0.0", "@electron/rebuild": "^3.2.10", "electron-packager": "^17.1.1", - "listr2": "^5.0.3", - "ora": "^5.0.0" + "listr2": "^5.0.3" }, "engines": { "node": ">= 14.17.5" diff --git a/packages/utils/types/src/index.ts b/packages/utils/types/src/index.ts index 602e303646..c30e21588e 100644 --- a/packages/utils/types/src/index.ts +++ b/packages/utils/types/src/index.ts @@ -1,6 +1,5 @@ import { ChildProcess } from 'child_process'; -import { OraImpl } from '@electron-forge/async-ora'; import { RebuildOptions } from '@electron/rebuild'; import { ArchOption, Options as ElectronPackagerOptions, TargetPlatform } from 'electron-packager'; import { ListrDefaultRenderer, ListrTask, ListrTaskWrapper } from 'listr2'; @@ -26,7 +25,6 @@ export interface ForgeSimpleHookSignatures { platform: ForgePlatform; arch: ForgeArch; outputPaths: string[]; - spinner?: OraImpl; } ]; preMake: []; @@ -56,9 +54,16 @@ export type ForgeHookFn = Hook extends keyof ForgeSi export type ForgeHookMap = { [S in ForgeHookName]?: ForgeHookFn; }; +export type ForgeMultiHookMap = { + [S in ForgeHookName]?: ForgeHookFn | ForgeHookFn[]; +}; export interface IForgePluginInterface { triggerHook(hookName: Hook, hookArgs: ForgeSimpleHookSignatures[Hook]): Promise; + getHookListrTasks( + hookName: Hook, + hookArgs: ForgeSimpleHookSignatures[Hook] + ): Promise; triggerMutatingHook( hookName: Hook, item: ForgeMutatingHookSignatures[Hook][0] @@ -122,7 +127,7 @@ export interface IForgePlugin { name: string; init(dir: string, forgeConfig: ResolvedForgeConfig): void; - getHooks?(): ForgeHookMap; + getHooks?(): ForgeMultiHookMap; startLogic?(opts: StartOptions): Promise; } @@ -194,7 +199,8 @@ export interface InitTemplateOptions { copyCIFiles?: boolean; } -export type ForgeListrTaskDefinition = ListrTask; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ForgeListrTaskDefinition = ListrTask; export interface ForgeTemplate { requiredForgeVersion?: string; diff --git a/tools/silent.js b/tools/silent.js new file mode 100644 index 0000000000..51e72ee36f --- /dev/null +++ b/tools/silent.js @@ -0,0 +1,12 @@ +const cp = require('child_process'); + +const [cmd, ...args] = process.argv.slice(2); + +const child = cp.spawn(cmd, args, { + stdio: 'pipe', +}); + +child.on('exit', (code, signal) => { + if (signal) process.exit(1); + else process.exit(code); +});