UNPKG

@electron-forge/core

Version:

A complete tool for building modern Electron applications

335 lines (295 loc) 11.9 kB
import path from 'path'; import { getElectronVersion } from '@electron-forge/core-utils'; import { MakerBase } from '@electron-forge/maker-base'; import { ForgeArch, ForgeConfigMaker, ForgeMakeResult, ForgePlatform, IForgeResolvableMaker, ResolvedForgeConfig } from '@electron-forge/shared-types'; import { getHostArch } from '@electron/get'; import chalk from 'chalk'; import filenamify from 'filenamify'; import fs from 'fs-extra'; import { Listr } from 'listr2'; import logSymbols from 'log-symbols'; import getForgeConfig from '../util/forge-config'; import { getHookListrTasks, runMutatingHook } from '../util/hook'; import getCurrentOutDir from '../util/out-dir'; import parseArchs from '../util/parse-archs'; import { readMutatedPackageJson } from '../util/read-package-json'; import requireSearch from '../util/require-search'; import resolveDir from '../util/resolve-dir'; import { listrPackage } from './package'; // eslint-disable-next-line @typescript-eslint/no-explicit-any class MakerImpl extends MakerBase<any> { name = 'impl'; defaultPlatforms = []; } type MakeTargets = ForgeConfigMaker[] | string[]; function generateTargets(forgeConfig: ResolvedForgeConfig, overrideTargets?: MakeTargets) { if (overrideTargets) { return overrideTargets.map((target) => { if (typeof target === 'string') { return forgeConfig.makers.find((maker) => (maker as IForgeResolvableMaker).name === target) || ({ name: target } as IForgeResolvableMaker); } return target; }); } return forgeConfig.makers; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function isElectronForgeMaker(target: MakerBase<any> | unknown): target is MakerBase<any> { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (target as MakerBase<any>).__isElectronForgeMaker; } type MakeContext = { dir: string; forgeConfig: ResolvedForgeConfig; actualOutDir: string; makers: MakerBase<unknown>[]; outputs: ForgeMakeResult[]; }; export interface MakeOptions { /** * The path to the app from which distrubutables are generated */ dir?: string; /** * Whether to use sensible defaults or prompt the user visually */ interactive?: boolean; /** * Whether to skip the pre-make packaging step */ skipPackage?: boolean; /** * An array of make targets to override your forge config */ overrideTargets?: MakeTargets; /** * The target architecture */ arch?: ForgeArch; /** * The target platform */ platform?: ForgePlatform; /** * The path to the directory containing generated distributables */ outDir?: string; } 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<MakeContext>( [ { title: 'Loading configuration', task: async (ctx) => { const resolvedDir = await resolveDir(providedDir); if (!resolvedDir) { throw new Error('Failed to locate startable Electron application'); } 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'.`); } // eslint-disable-next-line @typescript-eslint/no-explicit-any const makers: MakerBase<any>[] = []; const possibleMakers = generateTargets(forgeConfig, overrideTargets); for (const possibleMaker of possibleMakers) { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ let maker: MakerBase<any>; 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<typeof MakerImpl>(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 (!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}.`); } maker.ensureExternalBinariesExist(); makers.push(maker); } if (makers.length === 0) { throw new Error(`Could not find any make targets configured for the "${platform}" platform.`); } 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}`); } 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) { throw err; } else { throw new Error(`An unknown error occurred 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, } ); return runner; }; const make = async (opts: MakeOptions): Promise<ForgeMakeResult[]> => { const runner = listrMake(opts); await runner.run(); return runner.ctx.outputs; }; export default make;