@electron-forge/core
Version:
A complete tool for building modern Electron applications
406 lines (381 loc) • 14.4 kB
text/typescript
import path from 'node:path';
import { PublisherBase } from '@electron-forge/publisher-base';
import {
ForgeConfigPublisher,
ForgeListrOptions,
ForgeListrTask,
ForgeListrTaskFn,
ForgeMakeResult,
IForgePublisher,
IForgeResolvablePublisher,
ResolvedForgeConfig,
// ForgePlatform,
} from '@electron-forge/shared-types';
import { autoTrace, delayTraceTillSignal } from '@electron-forge/tracer';
import chalk from 'chalk';
import debug from 'debug';
import fs from 'fs-extra';
import { Listr } from 'listr2';
import getForgeConfig from '../util/forge-config';
import importSearch from '../util/import-search';
import getCurrentOutDir from '../util/out-dir';
import PublishState from '../util/publish-state';
import resolveDir from '../util/resolve-dir';
import { listrMake, MakeOptions } from './make';
const d = debug('electron-forge:publish');
type PublishContext = {
dir: string;
forgeConfig: ResolvedForgeConfig;
publishers: PublisherBase<unknown>[];
makeResults: ForgeMakeResult[];
};
export interface PublishOptions {
/**
* The path to the app to be published
*/
dir?: string;
/**
* Whether to use sensible defaults or prompt the user visually
*/
interactive?: boolean;
/**
* The publish targets, by default pulled from forge config, set this prop to
* override that list
*/
publishTargets?: ForgeConfigPublisher[] | string[];
/**
* Options object to passed through to make()
*/
makeOptions?: MakeOptions;
/**
* The path to the directory containing generated distributables
*/
outDir?: string;
/**
* Whether to generate dry run meta data but not actually publish
*/
dryRun?: boolean;
/**
* Whether or not to attempt to resume a previously saved `dryRun` and publish
*
* You can't use this combination at the same time as dryRun=true
*/
dryRunResume?: boolean;
}
export default autoTrace(
{ name: 'publish()', category: '@electron-forge/core' },
async (
childTrace,
{
dir: providedDir = process.cwd(),
interactive = false,
makeOptions = {},
publishTargets = undefined,
dryRun = false,
dryRunResume = false,
outDir,
}: PublishOptions,
): Promise<void> => {
if (dryRun && dryRunResume) {
throw new Error("Can't dry run and resume a dry run at the same time");
}
const listrOptions: ForgeListrOptions<PublishContext> = {
concurrent: false,
rendererOptions: {
collapseErrors: false,
},
silentRendererCondition: !interactive,
fallbackRendererCondition:
Boolean(process.env.DEBUG) || Boolean(process.env.CI),
};
const publishDistributablesTasks = (childTrace: typeof autoTrace) => [
{
title: 'Publishing distributables',
task: childTrace<Parameters<ForgeListrTaskFn<PublishContext>>>(
{ name: 'publish-distributables', category: '@electron-forge/core' },
async (
childTrace,
{ dir, forgeConfig, makeResults, publishers },
task: ForgeListrTask<PublishContext>,
) => {
if (publishers.length === 0) {
task.output = 'No publishers configured';
task.skip();
return;
}
return delayTraceTillSignal(
childTrace,
task.newListr<never>(
publishers.map((publisher) => ({
title: `${chalk.cyan(`[publisher-${publisher.name}]`)} Running the ${chalk.yellow('publish')} command`,
task: childTrace<Parameters<ForgeListrTaskFn>>(
{
name: `publish-${publisher.name}`,
category: '@electron-forge/core',
},
async (childTrace, _, task) => {
const setStatusLine = (s: string) => {
task.output = s;
};
await publisher.publish({
dir,
makeResults: makeResults!,
forgeConfig,
setStatusLine,
});
},
),
rendererOptions: {
persistentOutput: true,
},
})),
{
rendererOptions: {
collapseSubtasks: false,
collapseErrors: false,
},
},
),
'run',
);
},
),
rendererOptions: {
persistentOutput: true,
},
},
];
const runner = new Listr<PublishContext>(
[
{
title: 'Loading configuration',
task: childTrace<Parameters<ForgeListrTaskFn<PublishContext>>>(
{ name: 'load-forge-config', category: '@electron-forge/core' },
async (childTrace, ctx) => {
const resolvedDir = await resolveDir(providedDir);
if (!resolvedDir) {
throw new Error(
'Failed to locate publishable Electron application',
);
}
ctx.dir = resolvedDir;
ctx.forgeConfig = await getForgeConfig(resolvedDir);
},
),
},
{
title: 'Resolving publish targets',
task: childTrace<Parameters<ForgeListrTaskFn<PublishContext>>>(
{
name: 'resolve-publish-targets',
category: '@electron-forge/core',
},
async (childTrace, ctx, task) => {
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<any>;
if (
(publishTarget as IForgePublisher).__isElectronForgePublisher
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
publisher = publishTarget as PublisherBase<any>;
} else {
const resolvablePublishTarget =
publishTarget as IForgeResolvablePublisher;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const PublisherClass: any = await importSearch(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(', ')}`)}`;
}
},
),
rendererOptions: {
persistentOutput: true,
},
},
{
title: dryRunResume
? 'Resuming from dry run...'
: `Running ${chalk.yellow('make')} command`,
task: childTrace<Parameters<ForgeListrTaskFn<PublishContext>>>(
{
name: dryRunResume ? 'resume-dry-run' : 'make()',
category: '@electron-forge/core',
},
async (childTrace, 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 delayTraceTillSignal(
childTrace,
task.newListr<PublishContext>(
publishes.map((publishStates, index) => {
return {
title: `Publishing dry-run ${chalk.blue(`#${index + 1}`)}`,
task: childTrace<
Parameters<ForgeListrTaskFn<PublishContext>>
>(
{
name: `publish-dry-run-${index + 1}`,
category: '@electron-forge/core',
},
async (childTrace, ctx, task) => {
const restoredMakeResults = publishStates.map(
({ state }) => state,
);
d('restoring publish settings from dry run');
for (const makeResult of restoredMakeResults) {
makeResult.artifacts = await Promise.all(
makeResult.artifacts.map(
async (makePath: string) => {
// standardize the path to artifacts across platforms
const normalizedPath = makePath
.split(/\/|\\/)
.join(path.sep);
if (
!(await fs.pathExists(normalizedPath))
) {
throw new Error(
`Attempted to resume a dry run, but an artifact (${normalizedPath}) could not be found`,
);
}
return normalizedPath;
},
),
);
}
d('publishing for given state set');
return delayTraceTillSignal(
childTrace,
task.newListr(
publishDistributablesTasks(childTrace),
{
ctx: {
...ctx,
makeResults: restoredMakeResults,
},
rendererOptions: {
collapseSubtasks: false,
collapseErrors: false,
},
},
),
'run',
);
},
),
};
}),
{
rendererOptions: {
collapseSubtasks: false,
collapseErrors: false,
},
},
),
'run',
);
}
d('triggering make');
return delayTraceTillSignal(
childTrace,
listrMake(
childTrace,
{
dir,
interactive,
...makeOptions,
},
(results) => {
ctx.makeResults = results;
},
),
'run',
);
},
),
},
...(dryRunResume
? []
: dryRun
? [
{
title: 'Saving dry-run state',
task: childTrace<
Parameters<ForgeListrTaskFn<PublishContext>>
>(
{ name: 'save-dry-run', category: '@electron-forge/core' },
async (childTrace, { dir, forgeConfig, makeResults }) => {
d('saving results of make in dry run state', makeResults);
const calculatedOutDir =
outDir || getCurrentOutDir(dir, forgeConfig);
const dryRunDir = path.resolve(
calculatedOutDir,
'publish-dry-run',
);
await fs.remove(dryRunDir);
await PublishState.saveToDirectory(
dryRunDir,
makeResults!,
dir,
);
},
),
},
]
: publishDistributablesTasks(childTrace)),
],
listrOptions,
);
await runner.run();
},
);