@electron-forge/core
Version:
A complete tool for building modern Electron applications
314 lines (287 loc) • 9.63 kB
text/typescript
import { spawn, SpawnOptions } from 'node:child_process';
import readline from 'node:readline';
import {
getElectronVersion,
listrCompatibleRebuildHook,
} from '@electron-forge/core-utils';
import {
ElectronProcess,
ForgeArch,
ForgeListrOptions,
ForgeListrTaskFn,
ForgePlatform,
ResolvedForgeConfig,
StartOptions,
} from '@electron-forge/shared-types';
import { autoTrace, delayTraceTillSignal } from '@electron-forge/tracer';
import chalk from 'chalk';
import debug from 'debug';
import { Listr, PRESET_TIMER } from 'listr2';
import locateElectronExecutable from '../util/electron-executable';
import getForgeConfig from '../util/forge-config';
import { getHookListrTasks, runHook } from '../util/hook';
import { readMutatedPackageJson } from '../util/read-package-json';
import resolveDir from '../util/resolve-dir';
const d = debug('electron-forge:start');
export { StartOptions };
type StartContext = {
dir: string;
forgeConfig: ResolvedForgeConfig;
packageJSON: any;
spawned: ElectronProcess;
};
export default autoTrace(
{ name: 'start()', category: '@electron-forge/core' },
async (
childTrace,
{
dir: providedDir = process.cwd(),
appPath = '.',
interactive = false,
enableLogging = false,
args = [],
runAsNode = false,
inspect = false,
inspectBrk = false,
}: StartOptions,
): Promise<ElectronProcess> => {
const platform = process.env.npm_config_platform || process.platform;
const arch = process.env.npm_config_arch || process.arch;
const listrOptions: ForgeListrOptions<StartContext> = {
concurrent: false,
registerSignalListeners: false, // Don't re-render on SIGINT
rendererOptions: {
collapseErrors: false,
collapseSubtasks: false,
},
silentRendererCondition: !interactive,
fallbackRendererCondition:
Boolean(process.env.DEBUG) || Boolean(process.env.CI),
};
const runner = new Listr<StartContext>(
[
{
title: 'Locating application',
task: childTrace<Parameters<ForgeListrTaskFn<StartContext>>>(
{ name: 'locate-application', 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;
},
),
},
{
title: 'Loading configuration',
task: childTrace<Parameters<ForgeListrTaskFn<StartContext>>>(
{ name: 'load-forge-config', category: '@electron-forge/core' },
async (_, ctx) => {
const { dir } = ctx;
ctx.forgeConfig = await getForgeConfig(dir);
ctx.packageJSON = await readMutatedPackageJson(
dir,
ctx.forgeConfig,
);
if (!ctx.packageJSON.version) {
throw new Error(
`Please set your application's 'version' in '${dir}/package.json'.`,
);
}
},
),
},
{
title: 'Preparing native dependencies',
task: childTrace<Parameters<ForgeListrTaskFn<StartContext>>>(
{
name: 'prepare-native-dependencies',
category: '@electron-forge/core',
},
async (_, { dir, forgeConfig, packageJSON }, task) => {
await listrCompatibleRebuildHook(
dir,
await getElectronVersion(dir, packageJSON),
platform as ForgePlatform,
arch as ForgeArch,
forgeConfig.rebuildConfig,
task as any,
);
},
),
rendererOptions: {
persistentOutput: true,
bottomBar: Infinity,
timer: { ...PRESET_TIMER },
},
},
{
title: `Running ${chalk.yellow('generateAssets')} hook`,
task: childTrace<Parameters<ForgeListrTaskFn<StartContext>>>(
{
name: 'run-generateAssets-hook',
category: '@electron-forge/core',
},
async (childTrace, { forgeConfig }, task) => {
return delayTraceTillSignal(
childTrace,
task.newListr(
await getHookListrTasks(
childTrace,
forgeConfig,
'generateAssets',
platform,
arch,
),
),
'run',
);
},
),
},
{
title: `Running ${chalk.yellow('preStart')} hook`,
task: childTrace<Parameters<ForgeListrTaskFn<StartContext>>>(
{ name: 'run-preStart-hook', category: '@electron-forge/core' },
async (childTrace, { forgeConfig }, task) => {
return delayTraceTillSignal(
childTrace,
task.newListr(
await getHookListrTasks(childTrace, forgeConfig, 'preStart'),
),
'run',
);
},
),
},
{
task: (_ctx, task) => {
task.title = `${chalk.dim(`Launched Electron app. Type`)} ${chalk.bold('rs')} ${chalk.dim(`in terminal to restart main process.`)}`;
},
},
],
listrOptions,
);
await runner.run();
const { dir, forgeConfig, packageJSON } = runner.ctx;
let lastSpawned: ElectronProcess | null = null;
const forgeSpawn = async () => {
let electronExecPath: string | null = null;
// If a plugin has taken over the start command let's stop here
let spawnedPluginChild =
await forgeConfig.pluginInterface.overrideStartLogic({
dir,
appPath,
interactive,
enableLogging,
args,
runAsNode,
inspect,
inspectBrk,
});
if (
typeof spawnedPluginChild === 'object' &&
'tasks' in spawnedPluginChild
) {
const innerRunner = new Listr<never>(
[],
listrOptions as ForgeListrOptions<never>,
);
for (const task of spawnedPluginChild.tasks) {
innerRunner.add(task);
}
await innerRunner.run();
spawnedPluginChild = spawnedPluginChild.result;
}
let prefixArgs: string[] = [];
if (typeof spawnedPluginChild === 'string') {
electronExecPath = spawnedPluginChild;
} else if (Array.isArray(spawnedPluginChild)) {
[electronExecPath, ...prefixArgs] = spawnedPluginChild;
} else if (spawnedPluginChild) {
await runHook(forgeConfig, 'postStart', spawnedPluginChild);
return spawnedPluginChild;
}
if (!electronExecPath) {
electronExecPath = await locateElectronExecutable(dir, packageJSON);
}
d('Electron binary path:', electronExecPath);
const spawnOpts = {
cwd: dir,
stdio: 'inherit',
env: {
...process.env,
...(enableLogging
? {
ELECTRON_ENABLE_LOGGING: 'true',
ELECTRON_ENABLE_STACK_DUMPING: 'true',
}
: {}),
} as NodeJS.ProcessEnv,
};
if (runAsNode) {
spawnOpts.env.ELECTRON_RUN_AS_NODE = 'true';
} else {
delete spawnOpts.env.ELECTRON_RUN_AS_NODE;
}
if (inspect) {
args = ['--inspect' as string | number].concat(args);
}
if (inspectBrk) {
args = ['--inspect-brk' as string | number].concat(args);
}
const spawned = spawn(
electronExecPath!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
prefixArgs.concat([appPath]).concat(args as string[]),
spawnOpts as SpawnOptions,
) as ElectronProcess;
await runHook(forgeConfig, 'postStart', spawned);
return spawned;
};
const forgeSpawnWrapper = async () => {
const spawned = await forgeSpawn();
// When the child app is closed we should stop listening for stdin
if (spawned) {
if (interactive && process.stdin.isPaused()) {
process.stdin.resume();
}
spawned.on('exit', () => {
if (spawned.restarted) {
return;
}
if (interactive && !process.stdin.isPaused()) {
process.stdin.pause();
}
});
} else if (interactive && !process.stdin.isPaused()) {
process.stdin.pause();
}
lastSpawned = spawned;
return lastSpawned;
};
if (interactive) {
process.stdin.on('data', (data) => {
if (data.toString().trim() === 'rs' && lastSpawned) {
readline.moveCursor(process.stdout, 0, -1);
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
console.info(
`${chalk.green('✔ ')}${chalk.dim('Restarting Electron app')}`,
);
lastSpawned.restarted = true;
lastSpawned.on('exit', async () => {
lastSpawned!.emit('restarted', await forgeSpawnWrapper());
});
lastSpawned.kill('SIGTERM');
}
});
process.stdin.resume();
}
const spawned = await forgeSpawnWrapper();
if (interactive) console.log('');
return spawned;
},
);