@electron-forge/core
Version:
A complete tool for building modern Electron applications
441 lines (402 loc) • 15.1 kB
text/typescript
import path from 'node:path';
import { getHostArch } from '@electron/get';
import { getElectronVersion } from '@electron-forge/core-utils';
import { MakerBase } from '@electron-forge/maker-base';
import {
ForgeArch,
ForgeConfigMaker,
ForgeListrOptions,
ForgeListrTaskFn,
ForgeMakeResult,
ForgePlatform,
IForgeResolvableMaker,
ResolvedForgeConfig,
} from '@electron-forge/shared-types';
import { autoTrace, delayTraceTillSignal } from '@electron-forge/tracer';
import chalk from 'chalk';
import filenamify from 'filenamify';
import fs from 'fs-extra';
import { Listr, PRESET_TIMER } from 'listr2';
import logSymbols from 'log-symbols';
import getForgeConfig from '../util/forge-config';
import { getHookListrTasks, runMutatingHook } from '../util/hook';
import importSearch from '../util/import-search';
import getCurrentOutDir from '../util/out-dir';
import parseArchs from '../util/parse-archs';
import { readMutatedPackageJson } from '../util/read-package-json';
import resolveDir from '../util/resolve-dir';
import { listrPackage } from './package';
type MakerImpl = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new (...args: any[]): MakerBase<any>;
};
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: Array<() => 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 = (
childTrace: typeof autoTrace,
{
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: ForgeListrOptions<MakeContext> = {
concurrent: false,
rendererOptions: {
collapseSubtasks: false,
collapseErrors: false,
},
silentRendererCondition: !interactive,
fallbackRendererCondition:
Boolean(process.env.DEBUG) || Boolean(process.env.CI),
};
const runner = new Listr<MakeContext>(
[
{
title: 'Loading configuration',
task: childTrace<Parameters<ForgeListrTaskFn<MakeContext>>>(
{ name: 'load-forge-config', category: '@electron-forge/core' },
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: childTrace<Parameters<ForgeListrTaskFn<MakeContext>>>(
{ name: 'resolve-make-targets', category: '@electron-forge/core' },
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: Array<() => 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 = await importSearch<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.clone());
}
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(', ')}`)}`;
},
),
rendererOptions: {
persistentOutput: true,
},
},
{
title: `Running ${chalk.yellow('package')} command`,
task: childTrace<Parameters<ForgeListrTaskFn<MakeContext>>>(
{ name: 'package()', category: '@electron-forge/core' },
async (childTrace, ctx, task) => {
if (!skipPackage) {
return delayTraceTillSignal(
childTrace,
listrPackage(childTrace, {
dir: ctx.dir,
interactive,
arch,
outDir: ctx.actualOutDir,
platform,
}),
'run',
);
} else {
task.output = chalk.yellow(
`${logSymbols.warning} Skipping could result in an out of date build`,
);
task.skip();
}
},
),
rendererOptions: {
persistentOutput: true,
},
},
{
title: `Running ${chalk.yellow('preMake')} hook`,
task: childTrace<Parameters<ForgeListrTaskFn<MakeContext>>>(
{ name: 'run-preMake-hook', category: '@electron-forge/core' },
async (childTrace, ctx, task) => {
return delayTraceTillSignal(
childTrace,
task.newListr(
await getHookListrTasks(childTrace, ctx.forgeConfig, 'preMake'),
),
'run',
);
},
),
},
{
title: 'Making distributables',
task: childTrace<Parameters<ForgeListrTaskFn<MakeContext>>>(
{ name: 'make-distributables', category: '@electron-forge/core' },
async (childTrace, 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,
concurrent: true,
rendererOptions: {
collapseSubtasks: 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) {
const uniqMaker = maker();
subRunner.add({
title: `Making a ${chalk.magenta(uniqMaker.name)} distributable for ${chalk.cyan(`${platform}/${targetArch}`)}`,
task: childTrace<[]>(
{
name: `make-${maker.name}`,
category: '@electron-forge/core',
newRoot: true,
},
async () => {
try {
await Promise.resolve(
uniqMaker.prepareConfig(targetArch),
);
const artifacts = await uniqMaker.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 err;
} else if (typeof err === 'string') {
throw new Error(err);
} else {
throw new Error(
`An unknown error occurred while making for target: ${uniqMaker.name}`,
);
}
}
},
),
rendererOptions: {
timer: { ...PRESET_TIMER },
},
});
}
}
return delayTraceTillSignal(childTrace, subRunner, 'run');
},
),
},
{
title: `Running ${chalk.yellow('postMake')} hook`,
task: childTrace<Parameters<ForgeListrTaskFn<MakeContext>>>(
{ name: 'run-postMake-hook', category: '@electron-forge/core' },
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
const originalOutputs = JSON.stringify(ctx.outputs);
ctx.outputs = await runMutatingHook(
ctx.forgeConfig,
'postMake',
ctx.outputs,
);
let outputLocations = [path.resolve(ctx.actualOutDir, 'make')];
if (originalOutputs !== JSON.stringify(ctx.outputs)) {
const newDirs = new Set<string>();
const artifactPaths = [];
for (const result of ctx.outputs) {
for (const artifact of result.artifacts) {
newDirs.add(path.dirname(artifact));
artifactPaths.push(artifact);
}
}
if (newDirs.size <= ctx.outputs.length) {
outputLocations = [...newDirs];
} else {
outputLocations = artifactPaths;
}
}
receiveMakeResults?.(ctx.outputs);
task.output = `Artifacts available at: ${chalk.green(outputLocations.join(', '))}`;
},
),
rendererOptions: {
persistentOutput: true,
},
},
],
{
...listrOptions,
ctx: {} as MakeContext,
},
);
return runner;
};
export default autoTrace(
{ name: 'make()', category: '@electron-forge/core' },
async (childTrace, opts: MakeOptions): Promise<ForgeMakeResult[]> => {
const runner = listrMake(childTrace, opts);
await runner.run();
return runner.ctx.outputs;
},
);