@elsikora/setup-wizard
Version:
Setup Wizard - CLI scaffolding utility
433 lines (430 loc) • 22.8 kB
JavaScript
import { ESLINT_FEATURE_CONFIG } from '../../domain/constant/eslint-feature-config.constant.js';
import { ESLINT_FEATURE_GROUPS } from '../../domain/constant/eslint-feature-groups.constant.js';
import { EEslintFeature } from '../../domain/enum/eslint-feature.enum.js';
import { EFramework } from '../../domain/enum/framework.enum.js';
import { EModule } from '../../domain/enum/module.enum.js';
import { EPackageJsonDependencyType } from '../../domain/enum/package-json-dependency-type.enum.js';
import { NodeCommandService } from '../../infrastructure/service/node-command.service.js';
import { ESLINT_CONFIG } from '../constant/eslint/config.constant.js';
import { ESLINT_CONFIG_CORE_DEPENDENCIES } from '../constant/eslint/core-dependencies.constant.js';
import { ESLINT_CONFIG_ELSIKORA_PACKAGE_NAME } from '../constant/eslint/elsikora-package-name.constant.js';
import { ESLINT_CONFIG_ESLINT_MINIMUM_REQUIRED_VERSION } from '../constant/eslint/eslint-minimum-required-version.constant.js';
import { ESLINT_CONFIG_ESLINT_PACKAGE_NAME } from '../constant/eslint/eslint-package-name.constant.js';
import { ESLINT_CONFIG_FILE_NAME } from '../constant/eslint/file-name.constant.js';
import { ESLINT_CONFIG_FILE_NAMES } from '../constant/eslint/file-names.constant.js';
import { ESLINT_CONFIG_IGNORE_PATHS } from '../constant/eslint/ignore-paths.constant.js';
import { ESLINT_CONFIG_MESSAGES } from '../constant/eslint/messages.constant.js';
import { ESLINT_CONFIG_PACKAGE_JSON_SCRIPT_NAMES } from '../constant/eslint/package-json-script-names.constant.js';
import { ESLINT_CONFIG_SCRIPTS } from '../constant/eslint/scripts.constant.js';
import { ESLINT_CONFIG_SUMMARY } from '../constant/eslint/summary.constant.js';
import { FrameworkService } from './framework.service.js';
import { PackageJsonService } from './package-json.service.js';
/**
* Service for setting up and managing ESLint configuration.
* Handles the detection, installation, and configuration of ESLint features.
*/
class EslintModuleService {
/** CLI interface service for user interaction */
CLI_INTERFACE_SERVICE;
/** Command service for executing shell commands */
COMMAND_SERVICE;
/** Configuration service for managing app settings */
CONFIG_SERVICE;
/** File system service for file operations */
FILE_SYSTEM_SERVICE;
/** Service for managing package.json */
PACKAGE_JSON_SERVICE;
/** Cached ESLint configuration */
config = null;
/** Frameworks detected in the project */
detectedFrameworks = [];
/** Service for framework detection and configuration */
FRAMEWORK_SERVICE;
/** ESLint features selected by the user */
selectedFeatures = [];
/**
* Initializes a new instance of the ESLintModuleService.
* @param cliInterfaceService - Service for CLI user interactions
* @param fileSystemService - Service for file system operations
* @param configService - Service for managing app configuration
*/
constructor(cliInterfaceService, fileSystemService, configService) {
this.CLI_INTERFACE_SERVICE = cliInterfaceService;
this.FILE_SYSTEM_SERVICE = fileSystemService;
this.COMMAND_SERVICE = new NodeCommandService(cliInterfaceService);
this.PACKAGE_JSON_SERVICE = new PackageJsonService(fileSystemService, this.COMMAND_SERVICE);
this.FRAMEWORK_SERVICE = new FrameworkService(fileSystemService, this.PACKAGE_JSON_SERVICE);
this.CONFIG_SERVICE = configService;
}
/**
* Checks if the installed ESLint version meets the minimum requirements.
* Offers to update ESLint if the version is too old.
* @returns Promise resolving to true if ESLint version is acceptable, false otherwise
*/
async checkEslintVersion() {
const eslintVersion = await this.PACKAGE_JSON_SERVICE.getInstalledDependencyVersion(ESLINT_CONFIG_ESLINT_PACKAGE_NAME);
if (eslintVersion) {
const majorVersion = eslintVersion.majorVersion;
if (majorVersion < ESLINT_CONFIG_ESLINT_MINIMUM_REQUIRED_VERSION) {
this.CLI_INTERFACE_SERVICE.info(ESLINT_CONFIG_MESSAGES.eslintVersionLower(String(majorVersion), String(ESLINT_CONFIG_ESLINT_MINIMUM_REQUIRED_VERSION)));
const shouldRemove = await this.CLI_INTERFACE_SERVICE.confirm(ESLINT_CONFIG_MESSAGES.removeEslintVersion(String(majorVersion)), true);
if (!shouldRemove) {
this.CLI_INTERFACE_SERVICE.warn(ESLINT_CONFIG_MESSAGES.eslintUpdateCancelled);
return false;
}
this.CLI_INTERFACE_SERVICE.startSpinner(ESLINT_CONFIG_MESSAGES.uninstallingEslint);
await this.PACKAGE_JSON_SERVICE.uninstallPackages(ESLINT_CONFIG_ESLINT_PACKAGE_NAME);
this.CLI_INTERFACE_SERVICE.stopSpinner(ESLINT_CONFIG_MESSAGES.eslintUninstalledSuccessfully);
}
}
return true;
}
/**
* Handles existing ESLint setup.
* Checks for existing configuration and asks if user wants to remove it.
* @returns Promise resolving to true if setup should proceed, false otherwise
*/
async handleExistingSetup() {
const hasConfig = await this.PACKAGE_JSON_SERVICE.isExistsDependency(ESLINT_CONFIG_ELSIKORA_PACKAGE_NAME);
if (hasConfig) {
const shouldUninstall = await this.CLI_INTERFACE_SERVICE.confirm(ESLINT_CONFIG_MESSAGES.existingConfigDetected, true);
if (!shouldUninstall) {
this.CLI_INTERFACE_SERVICE.warn(ESLINT_CONFIG_MESSAGES.existingConfigAborted);
return false;
}
await this.uninstallExistingConfig();
}
const existingFiles = await this.findExistingConfigFiles();
if (existingFiles.length > 0) {
const filesList = existingFiles.map((f) => `- ${f}`).join("\n");
const shouldDelete = await this.CLI_INTERFACE_SERVICE.confirm(ESLINT_CONFIG_MESSAGES.existingFilesDetected(filesList), true);
if (shouldDelete) {
await Promise.all(existingFiles.map((file) => this.FILE_SYSTEM_SERVICE.deleteFile(file)));
}
else {
this.CLI_INTERFACE_SERVICE.warn(ESLINT_CONFIG_MESSAGES.existingFilesAborted);
return false;
}
}
return true;
}
/**
* Installs and configures ESLint.
* Guides the user through the setup process including feature selection.
* @returns Promise resolving to the module setup result
*/
async install() {
try {
this.config = await this.CONFIG_SERVICE.getModuleConfig(EModule.ESLINT);
if (!(await this.shouldInstall())) {
return { wasInstalled: false };
}
if (!(await this.handleExistingSetup())) {
return { wasInstalled: false };
}
if (!(await this.checkEslintVersion())) {
return { wasInstalled: false };
}
await this.detectFrameworks();
const savedFeatures = this.config?.features ?? [];
this.selectedFeatures = await this.selectFeatures(savedFeatures);
if (this.selectedFeatures.length === 0) {
this.CLI_INTERFACE_SERVICE.warn(ESLINT_CONFIG_MESSAGES.noFeaturesSelected);
return { wasInstalled: false };
}
if (!this.validateFeatureSelection()) {
return { wasInstalled: false };
}
await this.setupSelectedFeatures();
return {
customProperties: {
features: this.selectedFeatures,
},
wasInstalled: true,
};
}
catch (error) {
this.CLI_INTERFACE_SERVICE.handleError(ESLINT_CONFIG_MESSAGES.eslintSetupFailed, error);
throw error;
}
}
/**
* Determines if ESLint should be installed.
* Asks the user if they want to set up ESLint for their project.
* Uses the saved config value as default if it exists.
* @returns Promise resolving to true if ESLint should be installed, false otherwise
*/
async shouldInstall() {
try {
return await this.CLI_INTERFACE_SERVICE.confirm(ESLINT_CONFIG_MESSAGES.setupEslintPrompt, await this.CONFIG_SERVICE.isModuleEnabled(EModule.ESLINT));
}
catch (error) {
this.CLI_INTERFACE_SERVICE.handleError(ESLINT_CONFIG_MESSAGES.failedUserConfirmation, error);
return false;
}
}
/**
* Collects all required npm dependencies for selected ESLint features.
* @returns Array of package names to install
*/
collectDependencies() {
const dependencies = new Set(ESLINT_CONFIG_CORE_DEPENDENCIES);
for (const feature of this.selectedFeatures) {
const config = ESLINT_FEATURE_CONFIG[feature];
if (config.packages) {
for (const packageName of config.packages)
dependencies.add(packageName);
}
}
return [...dependencies];
}
/**
* Creates the ESLint configuration file.
* Generates a configuration with the selected features and ignore paths.
*/
async createConfig() {
const ignores = this.generateLintIgnorePaths();
await this.FILE_SYSTEM_SERVICE.writeFile(ESLINT_CONFIG_FILE_NAME, ESLINT_CONFIG.template(ignores, this.selectedFeatures), "utf8");
}
/**
* Detects frameworks used in the project.
* Identifies frameworks like React, Angular, TypeScript, etc.
*/
async detectFrameworks() {
this.CLI_INTERFACE_SERVICE.startSpinner(ESLINT_CONFIG_MESSAGES.detectingFrameworks);
try {
this.detectedFrameworks = await this.FRAMEWORK_SERVICE.detect();
if (this.detectedFrameworks.length > 0) {
const frameworkNames = this.detectedFrameworks.map((f) => f.displayName).join(", ");
this.CLI_INTERFACE_SERVICE.info(ESLINT_CONFIG_MESSAGES.detectedFrameworks(frameworkNames));
}
this.CLI_INTERFACE_SERVICE.stopSpinner(ESLINT_CONFIG_MESSAGES.frameworkDetectionCompleted);
}
catch (error) {
this.CLI_INTERFACE_SERVICE.stopSpinner(ESLINT_CONFIG_MESSAGES.failedDetectFrameworks);
throw error;
}
}
/**
* Detects ESLint features that should be installed based on project dependencies.
* Examines package.json and detected frameworks to determine appropriate features.
* @returns Promise resolving to an array of detected ESLint features
*/
async detectInstalledFeatures() {
const detectedFeatures = new Set();
const dependencies = {
...(await this.PACKAGE_JSON_SERVICE.getDependencies(EPackageJsonDependencyType.PROD)),
...(await this.PACKAGE_JSON_SERVICE.getDependencies(EPackageJsonDependencyType.DEV)),
};
const frameworkFeatures = this.FRAMEWORK_SERVICE.getFeatures(this.detectedFrameworks);
for (const feature of frameworkFeatures)
detectedFeatures.add(feature);
for (const [feature, config] of Object.entries(ESLINT_FEATURE_CONFIG)) {
if (config.isRequired) {
detectedFeatures.add(feature);
}
}
// Check if TypeScript is detected
let hasTypeScript = false;
for (const [feature, config] of Object.entries(ESLINT_FEATURE_CONFIG)) {
const eslintFeature = feature;
if ((eslintFeature === EEslintFeature.TYPESCRIPT || eslintFeature === EEslintFeature.TYPESCRIPT_STRICT) && config.detect?.some((packageName) => packageName in dependencies)) {
hasTypeScript = true;
break;
}
}
// If TypeScript is detected, prefer TYPESCRIPT_STRICT over TYPESCRIPT
if (hasTypeScript) {
detectedFeatures.add(EEslintFeature.TYPESCRIPT_STRICT);
}
// Add other features with detect property (excluding TypeScript features as we handled them above)
for (const [feature, config] of Object.entries(ESLINT_FEATURE_CONFIG)) {
const eslintFeature = feature;
if (eslintFeature !== EEslintFeature.TYPESCRIPT && eslintFeature !== EEslintFeature.TYPESCRIPT_STRICT && config.detect?.some((packageName) => packageName in dependencies)) {
detectedFeatures.add(eslintFeature);
}
}
return [...detectedFeatures];
}
/**
* Displays a summary of the ESLint setup results.
* Shows detected frameworks, selected features, and generated scripts.
*/
async displaySetupSummary() {
const packageJsonScripts = await this.PACKAGE_JSON_SERVICE.getProperty("scripts");
const packageJsonScriptsKeys = packageJsonScripts ? Object.keys(packageJsonScripts) : [];
const generatedScripts = ESLINT_CONFIG_PACKAGE_JSON_SCRIPT_NAMES.filter((script) => packageJsonScriptsKeys.includes(script));
const frameworksList = this.detectedFrameworks.length > 0
? this.detectedFrameworks.map((framework) => {
const description = framework.description ? `: ${framework.description}` : "";
return `- ${framework.displayName}${description}`;
})
: [ESLINT_CONFIG_MESSAGES.noFrameworksDetected];
const featuresList = this.selectedFeatures.map((feature) => `- ${feature}: ${ESLINT_FEATURE_CONFIG[feature].description}`);
const frameworkConfigs = this.detectedFrameworks.length > 0 ? [`${ESLINT_CONFIG_MESSAGES.lintPaths} ${this.FRAMEWORK_SERVICE.getLintPaths(this.detectedFrameworks).join(", ")}`] : [ESLINT_CONFIG_MESSAGES.noFrameworkConfigurations];
const scriptsList = generatedScripts.map((script) => `- npm run ${script}`);
const summary = [ESLINT_CONFIG_MESSAGES.eslintConfigCreated, "", ESLINT_CONFIG_MESSAGES.frameworksLabel, ...frameworksList, "", ESLINT_CONFIG_MESSAGES.installedFeaturesLabel, ...featuresList, "", ESLINT_CONFIG_MESSAGES.frameworkConfigurationsLabel, ...frameworkConfigs, "", ESLINT_CONFIG_MESSAGES.generatedScriptsLabel, ...scriptsList, "", ESLINT_CONFIG_MESSAGES.customizeInFile, `- ${ESLINT_CONFIG_FILE_NAME}`];
this.CLI_INTERFACE_SERVICE.note(ESLINT_CONFIG_SUMMARY.title, summary.join("\n"));
}
/**
* Finds existing ESLint configuration files.
* @returns Promise resolving to an array of file paths for existing configuration files
*/
async findExistingConfigFiles() {
const existingFiles = [];
for (const file of ESLINT_CONFIG_FILE_NAMES) {
if (await this.FILE_SYSTEM_SERVICE.isPathExists(file)) {
existingFiles.push(file);
}
}
return existingFiles;
}
/**
* Generates the ESLint command for linting.
* Creates a command string targeting appropriate directories based on detected frameworks.
* @returns The eslint command string
*/
generateLintCommand() {
const lintPaths = this.FRAMEWORK_SERVICE.getLintPaths(this.detectedFrameworks);
return ESLINT_CONFIG_SCRIPTS.lint.command(lintPaths);
}
/**
* Generates the ESLint command for fixing linting issues.
* Creates a command string with the --fix flag targeting appropriate directories.
* @returns The eslint fix command string
*/
generateLintFixCommand() {
const lintPaths = this.FRAMEWORK_SERVICE.getLintPaths(this.detectedFrameworks);
return ESLINT_CONFIG_SCRIPTS.lintFix.command(lintPaths);
}
/**
* Generates the list of paths to ignore in the ESLint configuration.
* @returns Array of ignore patterns for ESLint
*/
generateLintIgnorePaths() {
const ignorePatterns = this.getIgnorePatterns();
return ignorePatterns.length > 0 ? ignorePatterns : [];
}
/**
* Gets the patterns of files and directories to ignore during linting.
* Combines framework-specific ignore patterns with general ones.
* @returns Array of ignore patterns
*/
getIgnorePatterns() {
return [...this.FRAMEWORK_SERVICE.getIgnorePatterns(this.detectedFrameworks), ...ESLINT_CONFIG_IGNORE_PATHS];
}
/**
* Prompts the user to select which ESLint features to enable.
* Presents detected features and saved features as initial selections.
* @param savedFeatures - Previously saved ESLint features
* @returns Promise resolving to an array of selected ESLint features
*/
async selectFeatures(savedFeatures = []) {
const detectedFeatures = await this.detectInstalledFeatures();
let shouldUseDetected = false;
const hasValidSavedFeatures = savedFeatures.length > 0 && savedFeatures.every((feature) => Object.values(EEslintFeature).includes(feature));
if (!hasValidSavedFeatures && detectedFeatures.length > 1) {
shouldUseDetected = await this.CLI_INTERFACE_SERVICE.confirm(ESLINT_CONFIG_MESSAGES.detectedFeatures(detectedFeatures.join(", ")), true);
}
const groupedOptions = {};
for (const group of ESLINT_FEATURE_GROUPS) {
groupedOptions[group.name] = group.features.map((feature) => {
let label = `${feature} - ${ESLINT_FEATURE_CONFIG[feature].description}`;
// Add note about mutual exclusivity for TypeScript options
if (feature === EEslintFeature.TYPESCRIPT || feature === EEslintFeature.TYPESCRIPT_STRICT) {
label += " (choose one)";
}
return {
label,
value: feature,
};
});
}
const defaultFeatures = shouldUseDetected ? detectedFeatures : [];
const initialValues = hasValidSavedFeatures ? savedFeatures : defaultFeatures;
let selectedFeatures = await this.CLI_INTERFACE_SERVICE.groupMultiselect(ESLINT_CONFIG_MESSAGES.selectFeatures, groupedOptions, true, initialValues);
// Handle mutual exclusivity of TypeScript and TypeScript Strict
if (selectedFeatures.includes(EEslintFeature.TYPESCRIPT) && selectedFeatures.includes(EEslintFeature.TYPESCRIPT_STRICT)) {
this.CLI_INTERFACE_SERVICE.info(ESLINT_CONFIG_MESSAGES.typescriptStrictSelected);
selectedFeatures = selectedFeatures.filter((feature) => feature !== EEslintFeature.TYPESCRIPT);
}
return selectedFeatures;
}
/**
* Sets up npm scripts for ESLint.
* Adds scripts for linting, fixing, watching, and type checking.
*/
async setupScripts() {
await this.PACKAGE_JSON_SERVICE.addScript(ESLINT_CONFIG_SCRIPTS.lint.name, this.generateLintCommand());
await this.PACKAGE_JSON_SERVICE.addScript(ESLINT_CONFIG_SCRIPTS.lintFix.name, this.generateLintFixCommand());
if (this.detectedFrameworks.some((framework) => framework.isSupportWatch)) {
const lintPaths = this.FRAMEWORK_SERVICE.getLintPaths(this.detectedFrameworks);
await this.PACKAGE_JSON_SERVICE.addScript(ESLINT_CONFIG_SCRIPTS.lintWatch.name, ESLINT_CONFIG_SCRIPTS.lintWatch.command(lintPaths));
}
if (this.detectedFrameworks.some((framework) => framework.name === EFramework.TYPESCRIPT)) {
await this.PACKAGE_JSON_SERVICE.addScript(ESLINT_CONFIG_SCRIPTS.lintTypes.name, ESLINT_CONFIG_SCRIPTS.lintTypes.command([]));
await this.PACKAGE_JSON_SERVICE.addScript(ESLINT_CONFIG_SCRIPTS.lintTypesFix.name, ESLINT_CONFIG_SCRIPTS.lintTypesFix.command([]));
await this.PACKAGE_JSON_SERVICE.addScript(ESLINT_CONFIG_SCRIPTS.lintAll.name, ESLINT_CONFIG_SCRIPTS.lintAll.command([]));
await this.PACKAGE_JSON_SERVICE.addScript(ESLINT_CONFIG_SCRIPTS.lintAllFix.name, ESLINT_CONFIG_SCRIPTS.lintAllFix.command([]));
}
}
/**
* Sets up the selected ESLint features.
* Installs dependencies, creates config files, and sets up scripts.
*/
async setupSelectedFeatures() {
this.CLI_INTERFACE_SERVICE.startSpinner(ESLINT_CONFIG_MESSAGES.settingUpConfig);
try {
const packages = this.collectDependencies();
await this.PACKAGE_JSON_SERVICE.installPackages(packages, "latest", EPackageJsonDependencyType.DEV);
await this.createConfig();
await this.setupScripts();
this.CLI_INTERFACE_SERVICE.stopSpinner(ESLINT_CONFIG_MESSAGES.configurationCompleted);
await this.displaySetupSummary();
}
catch (error) {
this.CLI_INTERFACE_SERVICE.stopSpinner(ESLINT_CONFIG_MESSAGES.failedSetupConfig);
throw error;
}
}
/**
* Uninstalls existing ESLint configuration packages.
*/
async uninstallExistingConfig() {
this.CLI_INTERFACE_SERVICE.startSpinner(ESLINT_CONFIG_MESSAGES.uninstallingConfig);
try {
await this.PACKAGE_JSON_SERVICE.uninstallPackages([ESLINT_CONFIG_ELSIKORA_PACKAGE_NAME, ESLINT_CONFIG_ESLINT_PACKAGE_NAME]);
this.CLI_INTERFACE_SERVICE.stopSpinner(ESLINT_CONFIG_MESSAGES.existingConfigUninstalled);
}
catch (error) {
this.CLI_INTERFACE_SERVICE.stopSpinner(ESLINT_CONFIG_MESSAGES.failedUninstallConfig);
throw error;
}
}
/**
* Validates if the selected features are compatible with the detected frameworks.
* Checks if TypeScript features are selected only when TypeScript is detected.
* @returns Boolean indicating whether the feature selection is valid
*/
validateFeatureSelection() {
const errors = [];
// Check if both TYPESCRIPT and TYPESCRIPT_STRICT are selected
if (this.selectedFeatures.includes(EEslintFeature.TYPESCRIPT) && this.selectedFeatures.includes(EEslintFeature.TYPESCRIPT_STRICT)) {
errors.push(ESLINT_CONFIG_MESSAGES.cannotEnableBothTypescript);
}
for (const feature of this.selectedFeatures) {
const config = ESLINT_FEATURE_CONFIG[feature];
if (config.isRequiresTypescript && !this.detectedFrameworks.some((framework) => framework.name === EFramework.TYPESCRIPT)) {
errors.push(ESLINT_CONFIG_MESSAGES.featureRequiresTypescript(feature));
}
}
if (errors.length > 0) {
this.CLI_INTERFACE_SERVICE.warn(ESLINT_CONFIG_MESSAGES.configurationCannotProceed + errors.map((error) => `- ${error}`).join("\n"));
return false;
}
return true;
}
}
export { EslintModuleService };
//# sourceMappingURL=eslint-module.service.js.map