@electron-forge/core
Version:
A complete tool for building modern Electron applications
212 lines (195 loc) • 6.86 kB
text/typescript
import { PluginBase } from '@electron-forge/plugin-base';
import {
ForgeListrTaskDefinition,
ForgeMutatingHookFn,
ForgeMutatingHookSignatures,
ForgeSimpleHookFn,
ForgeSimpleHookSignatures,
IForgePlugin,
IForgePluginInterface,
ResolvedForgeConfig,
StartResult,
} from '@electron-forge/shared-types';
import { autoTrace } from '@electron-forge/tracer';
import chalk from 'chalk';
import debug from 'debug';
// eslint-disable-next-line n/no-missing-import
import { StartOptions } from '../api';
import importSearch from './import-search';
const d = debug('electron-forge:plugins');
function isForgePlugin(plugin: IForgePlugin | unknown): plugin is IForgePlugin {
return (plugin as IForgePlugin).__isElectronForgePlugin;
}
export default class PluginInterface implements IForgePluginInterface {
private plugins: IForgePlugin[] = [];
private _pluginPromise: Promise<void> = Promise.resolve();
private config: ResolvedForgeConfig;
static async create(
dir: string,
forgeConfig: ResolvedForgeConfig,
): Promise<PluginInterface> {
const int = new PluginInterface(dir, forgeConfig);
await int._pluginPromise;
return int;
}
private constructor(dir: string, forgeConfig: ResolvedForgeConfig) {
this._pluginPromise = Promise.all(
forgeConfig.plugins.map(async (plugin): Promise<IForgePlugin> => {
if (isForgePlugin(plugin)) {
return plugin;
}
if (
typeof plugin === 'object' &&
'name' in plugin &&
'config' in plugin
) {
const { name: pluginName, config: opts } = plugin;
if (typeof pluginName !== 'string') {
throw new Error(
`Expected plugin[0] to be a string but found ${pluginName}`,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Plugin = await importSearch<any>(dir, [pluginName]);
if (!Plugin) {
throw new Error(
`Could not find module with name: ${pluginName}. Make sure it's listed in the devDependencies of your package.json`,
);
}
return new Plugin(opts);
}
throw new Error(
`Expected plugin to either be a plugin instance or a { name, config } object but found ${JSON.stringify(plugin)}`,
);
}),
).then((plugins) => {
this.plugins = plugins;
for (const plugin of this.plugins) {
plugin.init(dir, forgeConfig);
}
return;
});
// TODO: fix hack
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.config = null as any;
Object.defineProperty(this, 'config', {
value: forgeConfig,
enumerable: false,
configurable: false,
writable: false,
});
this.triggerHook = this.triggerHook.bind(this);
this.overrideStartLogic = this.overrideStartLogic.bind(this);
}
async triggerHook<Hook extends keyof ForgeSimpleHookSignatures>(
hookName: Hook,
hookArgs: ForgeSimpleHookSignatures[Hook],
): Promise<void> {
for (const plugin of this.plugins) {
if (typeof plugin.getHooks === 'function') {
let hooks = plugin.getHooks()[hookName] as
| ForgeSimpleHookFn<Hook>[]
| ForgeSimpleHookFn<Hook>;
if (hooks) {
if (typeof hooks === 'function') hooks = [hooks];
for (const hook of hooks) {
await hook(this.config, ...hookArgs);
}
}
}
}
}
async getHookListrTasks<Hook extends keyof ForgeSimpleHookSignatures>(
childTrace: typeof autoTrace,
hookName: Hook,
hookArgs: ForgeSimpleHookSignatures[Hook],
): Promise<ForgeListrTaskDefinition[]> {
const tasks: ForgeListrTaskDefinition[] = [];
for (const plugin of this.plugins) {
if (typeof plugin.getHooks === 'function') {
let hooks = plugin.getHooks()[hookName] as
| ForgeSimpleHookFn<Hook>[]
| ForgeSimpleHookFn<Hook>;
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: childTrace(
{
name: 'forge-plugin-hook',
category: '@electron-forge/hooks',
extraDetails: { plugin: plugin.name, hook: hookName },
},
async (_, __, task) => {
if ((hook as any).__hookName) {
// Also give it the task
return await (hook as any).call(
task,
this.config,
...(hookArgs as any[]),
);
} else {
await hook(this.config, ...hookArgs);
}
},
),
rendererOptions: {},
});
}
}
}
}
return tasks;
}
async triggerMutatingHook<Hook extends keyof ForgeMutatingHookSignatures>(
hookName: Hook,
...item: ForgeMutatingHookSignatures[Hook]
): Promise<ForgeMutatingHookSignatures[Hook][0]> {
let result: ForgeMutatingHookSignatures[Hook][0] = item[0];
for (const plugin of this.plugins) {
if (typeof plugin.getHooks === 'function') {
let hooks = plugin.getHooks()[hookName] as
| ForgeMutatingHookFn<Hook>[]
| ForgeMutatingHookFn<Hook>;
if (hooks) {
if (typeof hooks === 'function') hooks = [hooks];
for (const hook of hooks) {
result = (await hook(this.config, ...item)) || result;
}
}
}
}
return result;
}
async overrideStartLogic(opts: StartOptions): Promise<StartResult> {
let newStartFn;
const claimed: string[] = [];
for (const plugin of this.plugins) {
if (
typeof plugin.startLogic === 'function' &&
plugin.startLogic !== PluginBase.prototype.startLogic
) {
claimed.push(plugin.name);
newStartFn = plugin.startLogic.bind(plugin);
}
}
if (claimed.length > 1) {
throw new Error(
`Multiple plugins tried to take control of the start command, please remove one of them\n --> ${claimed.join(', ')}`,
);
}
if (claimed.length === 1 && newStartFn) {
d(`plugin: "${claimed[0]}" has taken control of the start command`);
const result = await newStartFn(opts);
if (typeof result === 'object' && 'tasks' in result) {
result.tasks = result.tasks.map((task) => ({
...task,
title: `${chalk.cyan(`[plugin-${claimed[0]}]`)} ${task.title}`,
}));
}
return result;
}
return false;
}
}