UNPKG

@elsikora/setup-wizard

Version:

Setup Wizard - CLI scaffolding utility

366 lines (363 loc) 18.1 kB
#!/usr/bin/env node import { EModule } from '../../domain/enum/module.enum.js'; import { EPackageJsonDependencyType } from '../../domain/enum/package-json-dependency-type.enum.js'; import { ETestingFramework } from '../../domain/enum/testing-framework.enum.js'; import { NodeCommandService } from '../../infrastructure/service/node-command.service.js'; import { TESTING_CONFIG } from '../constant/testing/config.constant.js'; import { TESTING_CONFIG_MESSAGES } from '../constant/testing/messages.constant.js'; import { TESTING_UNIT_TEST_PATH, TESTING_E2E_TEST_PATH, TESTING_VITEST_CONFIG_FILES } from '../constant/testing/package-names.constant.js'; import { TESTING_CONFIG_SCRIPTS } from '../constant/testing/scripts.constant.js'; import { TESTING_FRAMEWORK_CONFIG } from '../constant/testing/testing-framework-config.constant.js'; import { PackageJsonService } from './package-json.service.js'; /** * Service for setting up and managing testing configuration. * Supports multiple testing frameworks through configuration. */ class TestingModuleService { /** CLI interface service for user interaction */ CLI_INTERFACE_SERVICE; /** Command service for executing shell commands */ COMMAND_SERVICE; /** Configuration service for managing app configuration */ CONFIG_SERVICE; /** File system service for file operations */ FILE_SYSTEM_SERVICE; /** Service for managing package.json */ PACKAGE_JSON_SERVICE; /** Cached testing configuration */ config = null; /** * Initializes a new instance of the TestingModuleService. * @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.CONFIG_SERVICE = configService; } /** * Handles existing testing setup. * Checks for existing configuration files and asks if user wants to remove them. * @returns Promise resolving to true if setup should proceed, false otherwise */ async handleExistingSetup() { const existingFiles = await this.findExistingConfigFiles(); if (existingFiles.length > 0) { const messageLines = [TESTING_CONFIG_MESSAGES.existingFilesDetected]; messageLines.push(""); for (const file of existingFiles) { messageLines.push(`- ${file}`); } messageLines.push("", TESTING_CONFIG_MESSAGES.deleteFilesQuestion); const shouldDelete = await this.CLI_INTERFACE_SERVICE.confirm(messageLines.join("\n"), true); if (shouldDelete) { await Promise.all(existingFiles.map((file) => this.FILE_SYSTEM_SERVICE.deleteFile(file))); } else { this.CLI_INTERFACE_SERVICE.warn(TESTING_CONFIG_MESSAGES.existingFilesAborted); return false; } } return true; } /** * Installs and configures the testing framework. * Guides the user through setting up testing configuration. * @returns Promise resolving to the module setup result */ async install() { try { this.config = await this.CONFIG_SERVICE.getModuleConfig(EModule.TESTING); if (!(await this.shouldInstall())) { return { wasInstalled: false }; } if (!(await this.handleExistingSetup())) { return { wasInstalled: false }; } const setupParameters = await this.setupTesting(); return { customProperties: setupParameters, wasInstalled: true, }; } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(TESTING_CONFIG_MESSAGES.failedSetupError, error); throw error; } } /** * Determines if the testing framework should be installed. * Asks the user if they want to set up testing configuration. * Uses the saved config value as default if it exists. * @returns Promise resolving to true if the module should be installed, false otherwise */ async shouldInstall() { try { return await this.CLI_INTERFACE_SERVICE.confirm(TESTING_CONFIG_MESSAGES.confirmSetup, await this.CONFIG_SERVICE.isModuleEnabled(EModule.TESTING)); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(TESTING_CONFIG_MESSAGES.failedConfirmation, error); return false; } } /** * Creates e2e test configuration file. * @param framework - The selected testing framework * @param isTypeScript - Whether the project uses TypeScript * @param isCoverageEnabled - Whether coverage is enabled */ async createEndToEndConfig(framework, isTypeScript, isCoverageEnabled) { const frameworkConfig = TESTING_FRAMEWORK_CONFIG[framework]; // For now, use the existing Vitest template logic // In the future, we'll add specific generators for each framework if (framework === ETestingFramework.VITEST) { await this.FILE_SYSTEM_SERVICE.writeFile(frameworkConfig.configFiles.e2e, TESTING_CONFIG.e2eConfigTemplate(isTypeScript, isCoverageEnabled), "utf8"); } } /** * Creates test directories if they don't exist. */ async createTestDirectories(isUnitEnabled, isEndToEndEnabled) { if (isUnitEnabled) { await this.FILE_SYSTEM_SERVICE.createDirectory(TESTING_UNIT_TEST_PATH, { isRecursive: true }); } if (isEndToEndEnabled) { await this.FILE_SYSTEM_SERVICE.createDirectory(TESTING_E2E_TEST_PATH, { isRecursive: true }); } } /** * Creates unit test configuration file. * @param framework - The selected testing framework * @param isTypeScript - Whether the project uses TypeScript * @param isCoverageEnabled - Whether coverage is enabled */ async createUnitConfig(framework, isTypeScript, isCoverageEnabled) { const frameworkConfig = TESTING_FRAMEWORK_CONFIG[framework]; // For now, use the existing Vitest template logic // In the future, we'll add specific generators for each framework if (framework === ETestingFramework.VITEST) { await this.FILE_SYSTEM_SERVICE.writeFile(frameworkConfig.configFiles.unit, TESTING_CONFIG.unitConfigTemplate(isTypeScript, isCoverageEnabled), "utf8"); } } /** * Displays a summary of the testing setup results. * Lists configuration options, generated scripts, and files. * @param framework - The selected testing framework * @param isUnitEnabled - Whether unit tests are enabled * @param isCoverageEnabled - Whether coverage is enabled * @param isEndToEndEnabled - Whether e2e tests are enabled */ displaySetupSummary(framework, isUnitEnabled, isCoverageEnabled, isEndToEndEnabled) { const frameworkConfig = TESTING_FRAMEWORK_CONFIG[framework]; const summary = [TESTING_CONFIG_MESSAGES.testingSummary, "", ` - Testing Framework: ${frameworkConfig.name}`]; if (isUnitEnabled) { summary.push(TESTING_CONFIG_MESSAGES.unitEnabled, TESTING_CONFIG_MESSAGES.unitIncludePattern); } if (isEndToEndEnabled) { summary.push(TESTING_CONFIG_MESSAGES.e2eEnabled, TESTING_CONFIG_MESSAGES.e2eIncludePattern); } if (isCoverageEnabled) { summary.push(TESTING_CONFIG_MESSAGES.coverageEnabled, TESTING_CONFIG_MESSAGES.coverageProvider, TESTING_CONFIG_MESSAGES.coverageReporter, TESTING_CONFIG_MESSAGES.coverageInclude); } summary.push("", TESTING_CONFIG_MESSAGES.generatedScriptsLabel); if (isUnitEnabled) { summary.push(TESTING_CONFIG_MESSAGES.testUnitDescription, TESTING_CONFIG_MESSAGES.testUnitWatchDescription); if (isCoverageEnabled) { summary.push(TESTING_CONFIG_MESSAGES.testUnitCoverageDescription); } } if (isEndToEndEnabled) { summary.push(TESTING_CONFIG_MESSAGES.testE2eDescription, TESTING_CONFIG_MESSAGES.testE2eWatchDescription); } if (isUnitEnabled && isEndToEndEnabled) { summary.push(TESTING_CONFIG_MESSAGES.testAllDescription); } summary.push("", TESTING_CONFIG_MESSAGES.generatedFilesLabel); if (isUnitEnabled) { summary.push(` - ${frameworkConfig.configFiles.unit}`); } if (isEndToEndEnabled) { summary.push(` - ${frameworkConfig.configFiles.e2e}`); } this.CLI_INTERFACE_SERVICE.note(TESTING_CONFIG_MESSAGES.setupCompleteTitle, summary.join("\n")); } /** * Finds existing testing configuration files. * @returns Promise resolving to an array of file paths for existing configuration files */ async findExistingConfigFiles() { const existingFiles = []; // Check for config files from all frameworks for (const framework of Object.values(ETestingFramework)) { const config = TESTING_FRAMEWORK_CONFIG[framework]; if (await this.FILE_SYSTEM_SERVICE.isPathExists(config.configFiles.unit)) { existingFiles.push(config.configFiles.unit); } if (await this.FILE_SYSTEM_SERVICE.isPathExists(config.configFiles.e2e)) { existingFiles.push(config.configFiles.e2e); } } // Check for generic config files for (const file of TESTING_VITEST_CONFIG_FILES) { if (await this.FILE_SYSTEM_SERVICE.isPathExists(file)) { existingFiles.push(file); } } return existingFiles; } /** * Prompts the user if they want to enable coverage. * @returns Promise resolving to true if coverage should be enabled */ async isCoverageEnabled() { const isConfirmedByDefault = this.config?.isCoverageEnabled ?? true; return await this.CLI_INTERFACE_SERVICE.confirm(TESTING_CONFIG_MESSAGES.confirmCoverage, isConfirmedByDefault); } /** * Prompts the user if they want to enable e2e tests. * @returns Promise resolving to true if e2e tests should be enabled */ async isEndToEndEnabled() { const isConfirmedByDefault = this.config?.isE2eEnabled ?? false; return await this.CLI_INTERFACE_SERVICE.confirm(TESTING_CONFIG_MESSAGES.confirmE2e, isConfirmedByDefault); } /** * Checks if the project uses TypeScript by looking for tsconfig.json. * @returns Promise resolving to true if TypeScript is detected */ async isTypeScriptProject() { return await this.FILE_SYSTEM_SERVICE.isPathExists("tsconfig.json"); } /** * Prompts the user if they want to enable unit tests. * @returns Promise resolving to true if unit tests should be enabled */ async isUnitEnabled() { const isConfirmedByDefault = this.config?.isUnitEnabled ?? true; return await this.CLI_INTERFACE_SERVICE.confirm(TESTING_CONFIG_MESSAGES.confirmUnit, isConfirmedByDefault); } /** * Prompts the user to select a testing framework. * @returns Promise resolving to the selected testing framework */ async selectTestingFramework() { const savedFramework = this.config?.framework; // Show all available frameworks const availableFrameworks = [ { description: TESTING_FRAMEWORK_CONFIG[ETestingFramework.VITEST].description, label: TESTING_FRAMEWORK_CONFIG[ETestingFramework.VITEST].name, value: ETestingFramework.VITEST, }, { description: TESTING_FRAMEWORK_CONFIG[ETestingFramework.JEST].description, label: TESTING_FRAMEWORK_CONFIG[ETestingFramework.JEST].name, value: ETestingFramework.JEST, }, { description: TESTING_FRAMEWORK_CONFIG[ETestingFramework.MOCHA].description, label: TESTING_FRAMEWORK_CONFIG[ETestingFramework.MOCHA].name, value: ETestingFramework.MOCHA, }, { description: TESTING_FRAMEWORK_CONFIG[ETestingFramework.JASMINE].description, label: TESTING_FRAMEWORK_CONFIG[ETestingFramework.JASMINE].name, value: ETestingFramework.JASMINE, }, ]; return await this.CLI_INTERFACE_SERVICE.select(TESTING_CONFIG_MESSAGES.selectFrameworkPrompt, availableFrameworks, savedFramework ?? ETestingFramework.VITEST); } /** * Sets up npm scripts for testing. * Adds scripts for running tests, coverage, and watch mode. * @param framework - The selected testing framework * @param isUnitEnabled - Whether to add unit test scripts * @param isCoverageEnabled - Whether to add coverage script * @param isEndToEndEnabled - Whether to add e2e test scripts */ async setupScripts(framework, isUnitEnabled, isCoverageEnabled, isEndToEndEnabled) { const frameworkConfig = TESTING_FRAMEWORK_CONFIG[framework]; // Unit test scripts if (isUnitEnabled) { await this.PACKAGE_JSON_SERVICE.addScript(TESTING_CONFIG_SCRIPTS.testUnit.name, TESTING_CONFIG_SCRIPTS.testUnit.command(framework, frameworkConfig.configFiles.unit)); await this.PACKAGE_JSON_SERVICE.addScript(TESTING_CONFIG_SCRIPTS.testUnitWatch.name, TESTING_CONFIG_SCRIPTS.testUnitWatch.command(framework, frameworkConfig.configFiles.unit)); if (isCoverageEnabled) { await this.PACKAGE_JSON_SERVICE.addScript(TESTING_CONFIG_SCRIPTS.testUnitCoverage.name, TESTING_CONFIG_SCRIPTS.testUnitCoverage.command(framework, frameworkConfig.configFiles.unit)); } } if (isEndToEndEnabled) { await this.PACKAGE_JSON_SERVICE.addScript(TESTING_CONFIG_SCRIPTS.testE2e.name, TESTING_CONFIG_SCRIPTS.testE2e.command(framework, frameworkConfig.configFiles.e2e)); await this.PACKAGE_JSON_SERVICE.addScript(TESTING_CONFIG_SCRIPTS.testE2eWatch.name, TESTING_CONFIG_SCRIPTS.testE2eWatch.command(framework, frameworkConfig.configFiles.e2e)); } if (isUnitEnabled && isEndToEndEnabled) { await this.PACKAGE_JSON_SERVICE.addScript(TESTING_CONFIG_SCRIPTS.testAll.name, TESTING_CONFIG_SCRIPTS.testAll.command()); } } /** * Sets up testing configuration. * Collects user input, installs dependencies, creates config files, * and sets up scripts. * @returns Promise resolving to an object containing setup parameters */ async setupTesting() { try { const parameters = {}; // Select testing framework const framework = await this.selectTestingFramework(); parameters.framework = framework; const frameworkConfig = TESTING_FRAMEWORK_CONFIG[framework]; // Detect TypeScript const isTypeScript = await this.isTypeScriptProject(); parameters.isTypeScript = isTypeScript; // Get configuration options from user const isUnitEnabled = await this.isUnitEnabled(); parameters.isUnitEnabled = isUnitEnabled; const isEndToEndEnabled = await this.isEndToEndEnabled(); parameters.isEndToEndEnabled = isEndToEndEnabled; // Only ask about coverage if at least one test type is enabled let isCoverageEnabled = false; if (isUnitEnabled || isEndToEndEnabled) { isCoverageEnabled = await this.isCoverageEnabled(); parameters.isCoverageEnabled = isCoverageEnabled; } // Install and configure this.CLI_INTERFACE_SERVICE.startSpinner(TESTING_CONFIG_MESSAGES.settingUpSpinner); // Create test directories await this.createTestDirectories(isUnitEnabled, isEndToEndEnabled); // Install core dependencies await this.PACKAGE_JSON_SERVICE.installPackages([...frameworkConfig.coreDependencies], "latest", EPackageJsonDependencyType.DEV); // Install optional dependencies const optionalDeps = []; if (isCoverageEnabled && frameworkConfig.optionalDependencies.coverage) { optionalDeps.push(frameworkConfig.optionalDependencies.coverage); } if (isTypeScript && frameworkConfig.optionalDependencies.typescript) { optionalDeps.push(frameworkConfig.optionalDependencies.typescript); } if (optionalDeps.length > 0) { await this.PACKAGE_JSON_SERVICE.installPackages(optionalDeps, "latest", EPackageJsonDependencyType.DEV); } // Create configuration files if (isUnitEnabled) { await this.createUnitConfig(framework, isTypeScript, isCoverageEnabled); } if (isEndToEndEnabled) { await this.createEndToEndConfig(framework, isTypeScript, isCoverageEnabled); } await this.setupScripts(framework, isUnitEnabled, isCoverageEnabled, isEndToEndEnabled); this.CLI_INTERFACE_SERVICE.stopSpinner(TESTING_CONFIG_MESSAGES.setupCompleteSpinner); this.displaySetupSummary(framework, isUnitEnabled, isCoverageEnabled, isEndToEndEnabled); return parameters; } catch (error) { this.CLI_INTERFACE_SERVICE.stopSpinner(TESTING_CONFIG_MESSAGES.failedSetupSpinner); throw error; } } } export { TestingModuleService }; //# sourceMappingURL=testing-module.service.js.map