@elsikora/setup-wizard
Version:
Setup Wizard - CLI scaffolding utility
398 lines (395 loc) • 20.4 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/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