UNPKG

@elsikora/setup-wizard

Version:

Setup Wizard - CLI scaffolding utility

398 lines (395 loc) 20.4 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/elint-config.constant.js'; import { ESLINT_CONFIG_CORE_DEPENDENCIES } from '../constant/eslint-config-core-dependencies.constant.js'; import { ESLINT_CONFIG_ELSIKORA_PACKAGE_NAME } from '../constant/eslint-config-elsikora-package-name.constant.js'; import { ESLINT_CONFIG_ESLINT_MINIMUM_REQUIRED_VERSION } from '../constant/eslint-config-eslint-minimum-required-version.constant.js'; import { ESLINT_CONFIG_ESLINT_PACKAGE_NAME } from '../constant/eslint-config-eslint-package-name.costant.js'; import { ESLINT_CONFIG_FILE_NAME } from '../constant/eslint-config-file-name.constant.js'; import { ESLINT_CONFIG_FILE_NAMES } from '../constant/eslint-config-file-names.constant.js'; import { ESLINT_CONFIG_IGNORE_PATHS } from '../constant/eslint-config-ignore-paths.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(`Detected ESLint version ${String(majorVersion)}, which is lower than required version ${String(ESLINT_CONFIG_ESLINT_MINIMUM_REQUIRED_VERSION)}.`); const shouldRemove = await this.CLI_INTERFACE_SERVICE.confirm(`Do you want to remove ESLint version ${String(majorVersion)} and install the latest version?`, true); if (!shouldRemove) { this.CLI_INTERFACE_SERVICE.warn("ESLint update cancelled. Setup cannot proceed with the current version."); return false; } this.CLI_INTERFACE_SERVICE.startSpinner("Uninstalling ESLint..."); await this.PACKAGE_JSON_SERVICE.uninstallPackages(ESLINT_CONFIG_ESLINT_PACKAGE_NAME); this.CLI_INTERFACE_SERVICE.stopSpinner("ESLint uninstalled successfully."); } } 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("An existing ElsiKora ESLint configuration is detected. Would you like to uninstall it?", true); if (!shouldUninstall) { this.CLI_INTERFACE_SERVICE.warn("Existing ElsiKora ESLint configuration detected. Setup aborted."); return false; } await this.uninstallExistingConfig(); } const existingFiles = await this.findExistingConfigFiles(); if (existingFiles.length > 0) { const filesList = existingFiles.map((f) => `- ${f}`).join("\n"); const message = `Existing ESLint configuration files detected:\n${filesList}\n\nDo you want to delete them?`; const shouldDelete = await this.CLI_INTERFACE_SERVICE.confirm(message, true); if (shouldDelete) { await Promise.all(existingFiles.map((file) => this.FILE_SYSTEM_SERVICE.deleteFile(file))); } else { this.CLI_INTERFACE_SERVICE.warn("Existing ESLint configuration files detected. Setup aborted."); 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("No features selected."); 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("Failed to complete ESLint setup", 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("Do you want to set up ESLint for your project?", await this.CONFIG_SERVICE.isModuleEnabled(EModule.ESLINT)); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError("Failed to get user confirmation", 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("Detecting frameworks..."); 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(`Detected frameworks: ${frameworkNames}`); } this.CLI_INTERFACE_SERVICE.stopSpinner("Framework detection completed"); } catch (error) { this.CLI_INTERFACE_SERVICE.stopSpinner("Failed to detect frameworks"); 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); } } for (const [feature, config] of Object.entries(ESLINT_FEATURE_CONFIG)) { if (config.detect?.some((packageName) => packageName in dependencies)) { detectedFeatures.add(feature); } } 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 = ["lint", "lint:fix", "lint:watch", "lint:types", "lint:types:fix", "lint:all", "lint:all:fix"].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}`; }) : ["No frameworks detected"]; const featuresList = this.selectedFeatures.map((feature) => `- ${feature}: ${ESLINT_FEATURE_CONFIG[feature].description}`); const frameworkConfigs = this.detectedFrameworks.length > 0 ? [`Lint Paths: ${this.FRAMEWORK_SERVICE.getLintPaths(this.detectedFrameworks).join(", ")}`] : ["No framework-specific configurations"]; const scriptsList = generatedScripts.map((script) => `- npm run ${script}`); const summary = ["ESLint configuration has been created.", "", "Detected Frameworks:", ...frameworksList, "", "Installed features:", ...featuresList, "", "Framework-specific configurations:", ...frameworkConfigs, "", "Generated scripts:", ...scriptsList, "", "You can customize the configuration in these file:", `- ${ESLINT_CONFIG_FILE_NAME}`]; this.CLI_INTERFACE_SERVICE.note("ESLint Setup", 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 ${lintPaths.length > 0 ? lintPaths.join(" ") : "."}`; } /** * 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 --fix ${lintPaths.length > 0 ? lintPaths.join(" ") : "."}`; } /** * 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(`Detected features: ${detectedFeatures.join(", ")}. Would you like to include these features?`, true); } const groupedOptions = {}; for (const group of ESLINT_FEATURE_GROUPS) { groupedOptions[group.name] = group.features.map((feature) => ({ label: `${feature} - ${ESLINT_FEATURE_CONFIG[feature].description}`, value: feature, })); } const defaultFeatures = shouldUseDetected ? detectedFeatures : []; const initialValues = hasValidSavedFeatures ? savedFeatures : defaultFeatures; return await this.CLI_INTERFACE_SERVICE.groupMultiselect("Select the features you want to enable:", groupedOptions, true, initialValues); } /** * Sets up npm scripts for ESLint. * Adds scripts for linting, fixing, watching, and type checking. */ async setupScripts() { await this.PACKAGE_JSON_SERVICE.addScript("lint", this.generateLintCommand()); await this.PACKAGE_JSON_SERVICE.addScript("lint:fix", this.generateLintFixCommand()); if (this.detectedFrameworks.some((framework) => framework.isSupportWatch)) { const lintPaths = this.FRAMEWORK_SERVICE.getLintPaths(this.detectedFrameworks); await this.PACKAGE_JSON_SERVICE.addScript("lint:watch", `npx eslint-watch ${lintPaths.join(" ")}`); } if (this.detectedFrameworks.some((framework) => framework.name === EFramework.TYPESCRIPT)) { await this.PACKAGE_JSON_SERVICE.addScript("lint:types", "tsc --noEmit"); await this.PACKAGE_JSON_SERVICE.addScript("lint:types:fix", "tsc --noEmit --skipLibCheck"); await this.PACKAGE_JSON_SERVICE.addScript("lint:all", "npm run lint && npm run lint:types"); await this.PACKAGE_JSON_SERVICE.addScript("lint:all:fix", "npm run lint:fix && npm run lint:types:fix"); } } /** * Sets up the selected ESLint features. * Installs dependencies, creates config files, and sets up scripts. */ async setupSelectedFeatures() { this.CLI_INTERFACE_SERVICE.startSpinner("Setting up ESLint configuration..."); 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 configuration completed successfully!"); await this.displaySetupSummary(); } catch (error) { this.CLI_INTERFACE_SERVICE.stopSpinner("Failed to setup ESLint configuration"); throw error; } } /** * Uninstalls existing ESLint configuration packages. */ async uninstallExistingConfig() { this.CLI_INTERFACE_SERVICE.startSpinner("Uninstalling existing ESLint configuration..."); try { await this.PACKAGE_JSON_SERVICE.uninstallPackages([ESLINT_CONFIG_ELSIKORA_PACKAGE_NAME, ESLINT_CONFIG_ESLINT_PACKAGE_NAME]); this.CLI_INTERFACE_SERVICE.stopSpinner("Existing ESLint configuration uninstalled successfully!"); } catch (error) { this.CLI_INTERFACE_SERVICE.stopSpinner("Failed to uninstall existing ESLint configuration"); 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 = []; 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(`${feature} requires TypeScript, but TypeScript is not detected in your project.`); } } if (errors.length > 0) { this.CLI_INTERFACE_SERVICE.warn("Configuration cannot proceed due to the following errors:\n" + errors.map((error) => `- ${error}`).join("\n")); return false; } return true; } } export { EslintModuleService }; //# sourceMappingURL=eslint-module.service.js.map