UNPKG

@elsikora/setup-wizard

Version:

Setup Wizard - CLI scaffolding utility

433 lines (430 loc) 22.8 kB
#!/usr/bin/env node 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