create-electron-foundation
Version:
An interactive CLI to bootstrap a modern, type-safe, and scalable Electron application.
187 lines (186 loc) • 9.63 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import * as p from '@clack/prompts';
// FUNCTIONS
import { renderTitle } from './utils/renderTitle.js';
import { runUserPromptCli } from './cli/index.js';
import { buildPkgInstallerMap } from './installers/buildPkgInstallerMap.js';
import { scaffoldProject } from './helpers/scaffoldProject.js';
import { installPackages } from './helpers/installPackages.js';
import { selectBoilerplate } from './helpers/selectBoilerplate.js';
import { parseCliArgs } from './helpers/parseCliArgs.js';
// UTILS
import { logger } from './utils/logger.js';
import { handleError, CLIError } from './utils/errors.js';
// TERMINAL
import { execa } from 'execa';
import ora from 'ora';
import chalk from 'chalk';
const main = async () => {
/**
* ⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
* Main entry point for the Create Electron Foundation CLI
*
* Steps:
* 1. PARSE command line arguments for configurations
* 2. PROMPT user to fill configurations
* 3. CONFIGURE inUse packages & custom installers
* 4. SCAFFOLD base project (configuration agnostic files)
* 5. INSTALL packages (update the package.json - not 'npm i')
* 6. SELECT boilerplate (⭐️ most important file -- copies the configuration specific files into the users project)
* 7. UPDATE package.json
* 8. INITIALIZE git
* 9. OPEN in IDE
* ⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
* ####################################################################### */
// 1. PARSE: command line arguments #########################################
const cliArgs = await parseCliArgs(process.argv);
renderTitle();
logger.info(`Node.js version: ${process.version}`);
// 2. PROMPT: user to fill configurations ###################################
const config = await runUserPromptCli(cliArgs);
// Display summary to ensure it's visible to the user
p.note(`
Project Name: ${chalk.blue.bold(config.project_name)}
Router: ${chalk.green.bold(config?.packages?.router)}
Styles: ${chalk.green.bold(config?.packages?.styles || 'Vanilla CSS')}
Database: ${config?.packages?.database ? chalk.green.bold(config?.packages?.database) : chalk.red.bold('false')}
ORM: ${config?.packages?.orm ? chalk.green.bold(config?.packages?.orm) : chalk.red.bold('false')}
Initialize Git: ${config.initialize_git ? chalk.green.bold('true') : chalk.red.bold('false')}
Install Packages: ${config.install_packages ? chalk.green.bold('true') : chalk.red.bold('false')}
IDE: ${config.ide ? chalk.green.bold(config.ide) : chalk.red.bold('false')}
`, 'Summary of your choices:');
// INJECT ENV VARIABLES ######################################################
if (cliArgs.project_name) {
process.env.APP_NAME = cliArgs.project_name;
process.env.CEF_FRAMEWORK = 'Electron';
process.env.CEF_ROUTER = cliArgs.router;
process.env.CEF_STYLES = cliArgs.styles || 'Vanilla CSS';
if (cliArgs.database) {
process.env.CEF_DATABASE = cliArgs.database;
}
if (cliArgs.orm) {
process.env.CEF_ORM = cliArgs.orm;
}
}
// 3. CONFIGURE: inUse packages & custom installers #########################
const inUsePackages = Object.values(config.packages)
.flat()
.filter((pkg) => !!pkg);
const usePackages = buildPkgInstallerMap(config.project_name, inUsePackages);
// 4. SCAFFOLD: base project (configuration agnostic files) ##################
await scaffoldProject(config);
// 5. INSTALL: packages (update the package.json - not 'npm i') #############
installPackages({
...config,
packages: usePackages,
});
// 6. SELECT: boilerplate ###################################################
// ⭐️ most important file -- copies the configuration specific files into the users project ⭐️
selectBoilerplate(config);
// 7. UPDATE: package.json ###################################################
const pkgJson = fs.readJSONSync(path.join(config.project_dir, 'package.json'));
pkgJson.name = config.project_name;
fs.writeJSONSync(path.join(config.project_dir, 'package.json'), pkgJson, {
spaces: 2,
});
// 8. INSTALL PACKAGES ######################################################
if (!cliArgs.ci && config.install_packages) {
const projectSetupSpinner = ora(`${chalk.blue(config.project_name)} ${chalk.green.bold('Installing Packages')}...`).start();
let command = `cd ${config.project_dir} && make ri`;
if (process.platform === 'darwin') {
/**
# ------------------------------------------------------------------------------
# macOS/nvm Compatibility Fix
#
# Problem:
# On macOS, if Node.js is installed via Homebrew, it can set a global
# `npm_config_prefix` environment variable. This variable conflicts with nvm
# (Node Version Manager), which this Makefile uses for Node version management.
# When nvm detects this variable, it throws an error and exits to prevent
# potential package corruption.
#
# Solution:
# This block conditionally prepares a command to `unset` the conflicting
# environment variable *only on macOS*.
#
# - `uname -s`: This command gets the name of the operating system kernel.
# On macOS, it returns "Darwin".
# - `ifeq ... endif`: This is a makefile conditional. The code inside only runs
# if the condition is met.
# - `UNSET_NPM_CONFIG_PREFIX`: We define a variable that is empty by default.
# On Darwin systems, we set it to `unset npm_config_prefix &&`.
#
# This variable is then prepended to any command that uses `npm` or `nvm`,
# ensuring the conflict is resolved for that specific command without
# permanently altering the user's shell configuration.
# ------------------------------------------------------------------------------
*/
command = `cd ${config.project_dir} && unset npm_config_prefix && make ri`;
}
try {
await execa(command, {
shell: true,
});
projectSetupSpinner.succeed(`${chalk.blue(config.project_name)} ${chalk.bold.green('Packages installed')} successfully!`);
}
catch (err) {
projectSetupSpinner.fail(`${chalk.blue(config.project_name)} ${chalk.red('Packages installation failed!')}`);
throw new CLIError(`Package installation failed: ${command}`, 'PACKAGE_INSTALL_ERROR', false, err instanceof Error ? err : new Error(String(err)));
}
}
// 9. INITIALIZE: git #######################################################
if (config.initialize_git && !cliArgs.ci) {
const initializeGitSpinner = ora({
text: 'Initializing Git...',
spinner: 'dots',
}).start();
try {
await execa('git', ['init'], { cwd: config.project_dir });
await execa('git', ['add', '.'], { cwd: config.project_dir });
await execa('git', ['commit', '-m', 'Initial Scaffolding from create-electron-foundation'], {
cwd: config.project_dir,
});
initializeGitSpinner.succeed(`${chalk.blue(config.project_name)} ${chalk.bold.green('Git initialized')} successfully!`);
}
catch (err) {
initializeGitSpinner.fail(`${chalk.blue(config.project_name)} ${chalk.red('Git initialization')} failed!`);
throw new CLIError('Git initialization failed', 'GIT_INIT_ERROR', true, err instanceof Error ? err : new Error(String(err)));
}
}
// 10. OPEN: in IDE ##########################################################
if (config.ide && !cliArgs.ci) {
let command = `cd "${config.project_name}"`;
if (config.ide === 'cursor') {
command += ` && ${config.ide} .`;
}
else if (config.ide === 'vscode') {
command += ` && code .`;
}
try {
await execa(command, {
shell: true,
});
}
catch (err) {
logger.error(`Failed to execute: ${command}`);
logger.error(err);
if (err instanceof Error) {
logger.error(err.message);
}
}
}
logger.success(`${chalk.blue(config.project_name)} ${chalk.bold.green('Project Scaffolded Successfully')} ${chalk.white('with')} ${chalk.blue('create-electron-foundation!')}`);
};
main().catch((error) => {
const cliError = handleError(error);
logger.error(`❌ ${cliError.message}`);
if (cliError.cause && cliError.cause.stack) {
logger.debug('Stack trace:', cliError.cause.stack);
}
if (cliError.recoverable) {
logger.info('💡 This error might be recoverable. Please check your configuration and try again.');
}
process.exit(cliError.code === 'VALIDATION_ERROR' ? 2 : 1);
});