@elsikora/setup-wizard
Version:
Setup Wizard - CLI scaffolding utility
366 lines (363 loc) • 18.1 kB
JavaScript
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