@electron-forge/core
Version:
A complete tool for building modern Electron applications
271 lines (241 loc) • 11.2 kB
text/typescript
import path from 'path';
import { safeYarnOrNpm, updateElectronDependency } from '@electron-forge/core-utils';
import baseTemplate from '@electron-forge/template-base';
import chalk from 'chalk';
import debug from 'debug';
import fs from 'fs-extra';
import { Listr } from 'listr2';
import { merge } from 'lodash';
import installDepList, { DepType, DepVersionRestriction } from '../util/install-dependencies';
import { readRawPackageJson } from '../util/read-package-json';
import upgradeForgeConfig, { updateUpgradedForgeDevDeps } from '../util/upgrade-forge-config';
import { initGit } from './init-scripts/init-git';
import { deps, devDeps, exactDevDeps } from './init-scripts/init-npm';
const d = debug('electron-forge:import');
export interface ImportOptions {
/**
* The path to the app to be imported
*/
dir?: string;
/**
* Whether to use sensible defaults or prompt the user visually
*/
interactive?: boolean;
/**
* An async function that returns true or false in order to confirm the start
* of importing
*/
confirmImport?: () => Promise<boolean>;
/**
* An async function that returns whether the import should continue if it
* looks like a forge project already
*/
shouldContinueOnExisting?: () => Promise<boolean>;
/**
* An async function that returns whether the given dependency should be removed
*/
shouldRemoveDependency?: (dependency: string, explanation: string) => Promise<boolean>;
/**
* An async function that returns whether the given script should be overridden with a forge one
*/
shouldUpdateScript?: (scriptName: string, newValue: string) => Promise<boolean>;
/**
* The path to the directory containing generated distributables
*/
outDir?: string;
}
export default async ({
dir = process.cwd(),
interactive = false,
confirmImport,
shouldContinueOnExisting,
shouldRemoveDependency,
shouldUpdateScript,
outDir,
}: ImportOptions): Promise<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(
[
{
title: 'Locating importable project',
task: async () => {
d(`Attempting to import project in: ${dir}`);
if (!(await fs.pathExists(dir)) || !(await fs.pathExists(path.resolve(dir, 'package.json')))) {
throw new Error(`We couldn't find a project with a package.json file in: ${dir}`);
}
if (typeof confirmImport === 'function') {
if (!(await confirmImport())) {
// TODO: figure out if we can just return early here
// eslint-disable-next-line no-process-exit
process.exit(0);
}
}
await initGit(dir);
},
},
{
title: 'Processing configuration and dependencies',
options: {
persistentOutput: true,
bottomBar: Infinity,
},
task: async (ctx, task) => {
const calculatedOutDir = outDir || 'out';
const importDeps = ([] as string[]).concat(deps);
let importDevDeps = ([] as string[]).concat(devDeps);
let importExactDevDeps = ([] as string[]).concat(exactDevDeps);
let packageJSON = await readRawPackageJson(dir);
if (!packageJSON.version) {
task.output = chalk.yellow(`Please set the ${chalk.green('"version"')} in your application's package.json`);
}
if (packageJSON.config && packageJSON.config.forge) {
if (packageJSON.config.forge.makers) {
task.output = chalk.green('Existing Electron Forge configuration detected');
if (typeof shouldContinueOnExisting === 'function') {
if (!(await shouldContinueOnExisting())) {
// TODO: figure out if we can just return early here
// eslint-disable-next-line no-process-exit
process.exit(0);
}
}
} else if (!(typeof packageJSON.config.forge === 'object')) {
task.output = chalk.yellow(
"We can't tell if the Electron Forge config is compatible because it's in an external JavaScript file, not trying to convert it and continuing anyway"
);
} else {
d('Upgrading an Electron Forge < 6 project');
packageJSON.config.forge = upgradeForgeConfig(packageJSON.config.forge);
importDevDeps = updateUpgradedForgeDevDeps(packageJSON, importDevDeps);
}
}
packageJSON.dependencies = packageJSON.dependencies || {};
packageJSON.devDependencies = packageJSON.devDependencies || {};
[importDevDeps, importExactDevDeps] = updateElectronDependency(packageJSON, importDevDeps, importExactDevDeps);
const keys = Object.keys(packageJSON.dependencies).concat(Object.keys(packageJSON.devDependencies));
const buildToolPackages: Record<string, string | undefined> = {
'@electron/get': 'already uses this module as a transitive dependency',
'@electron/osx-sign': 'already uses this module as a transitive dependency',
'electron-builder': 'provides mostly equivalent functionality',
'electron-download': 'already uses this module as a transitive dependency',
'electron-forge': 'replaced with @electron-forge/cli',
'electron-installer-debian': 'already uses this module as a transitive dependency',
'electron-installer-dmg': 'already uses this module as a transitive dependency',
'electron-installer-flatpak': 'already uses this module as a transitive dependency',
'electron-installer-redhat': 'already uses this module as a transitive dependency',
'electron-packager': 'already uses this module as a transitive dependency',
'electron-winstaller': 'already uses this module as a transitive dependency',
};
for (const key of keys) {
if (buildToolPackages[key]) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const explanation = buildToolPackages[key]!;
let remove = true;
if (typeof shouldRemoveDependency === 'function') {
remove = await shouldRemoveDependency(key, explanation);
}
if (remove) {
delete packageJSON.dependencies[key];
delete packageJSON.devDependencies[key];
}
}
}
packageJSON.scripts = packageJSON.scripts || {};
d('reading current scripts object:', packageJSON.scripts);
const updatePackageScript = async (scriptName: string, newValue: string) => {
if (packageJSON.scripts[scriptName] !== newValue) {
let update = true;
if (typeof shouldUpdateScript === 'function') {
update = await shouldUpdateScript(scriptName, newValue);
}
if (update) {
packageJSON.scripts[scriptName] = newValue;
}
}
};
await updatePackageScript('start', 'electron-forge start');
await updatePackageScript('package', 'electron-forge package');
await updatePackageScript('make', 'electron-forge make');
d('forgified scripts object:', packageJSON.scripts);
const writeChanges = async () => {
await fs.writeJson(path.resolve(dir, 'package.json'), packageJSON, { spaces: 2 });
};
return task.newListr(
[
{
title: 'Installing dependencies',
task: async (_, task) => {
const packageManager = safeYarnOrNpm();
await writeChanges();
d('deleting old dependencies forcefully');
await fs.remove(path.resolve(dir, 'node_modules/.bin/electron'));
await fs.remove(path.resolve(dir, 'node_modules/.bin/electron.cmd'));
d('installing dependencies');
task.output = `${packageManager} install ${importDeps.join(' ')}`;
await installDepList(dir, importDeps);
d('installing devDependencies');
task.output = `${packageManager} install --dev ${importDevDeps.join(' ')}`;
await installDepList(dir, importDevDeps, DepType.DEV);
d('installing exactDevDependencies');
task.output = `${packageManager} install --dev --exact ${importExactDevDeps.join(' ')}`;
await installDepList(dir, importExactDevDeps, DepType.DEV, DepVersionRestriction.EXACT);
},
},
{
title: 'Copying base template Forge configuration',
task: async () => {
const pathToTemplateConfig = path.resolve(baseTemplate.templateDir, 'forge.config.js');
// if there's an existing config.forge object in package.json
if (packageJSON?.config?.forge && typeof packageJSON.config.forge === 'object') {
d('detected existing Forge config in package.json, merging with base template Forge config');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const templateConfig = require(path.resolve(baseTemplate.templateDir, 'forge.config.js'));
packageJSON = await readRawPackageJson(dir);
merge(templateConfig, packageJSON.config.forge); // mutates the templateConfig object
await writeChanges();
// otherwise, write to forge.config.js
} else {
d('writing new forge.config.js');
await fs.copyFile(pathToTemplateConfig, path.resolve(dir, 'forge.config.js'));
}
},
},
{
title: 'Fixing .gitignore',
task: async () => {
if (await fs.pathExists(path.resolve(dir, '.gitignore'))) {
const gitignore = await fs.readFile(path.resolve(dir, '.gitignore'));
if (!gitignore.includes(calculatedOutDir)) {
await fs.writeFile(path.resolve(dir, '.gitignore'), `${gitignore}\n${calculatedOutDir}/`);
}
}
},
},
],
listrOptions
);
},
},
{
title: 'Finalizing import',
options: {
persistentOutput: true,
bottomBar: Infinity,
},
task: (_, task) => {
task.output = `We have attempted to convert your app to be in a format that Electron Forge understands.
Thanks for using ${chalk.green('Electron Forge')}!`;
},
},
],
listrOptions
);
await runner.run();
};