UNPKG

@elsikora/setup-wizard

Version:

Setup Wizard - CLI scaffolding utility

343 lines (340 loc) 17.2 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 { NodeCommandService } from '../../infrastructure/service/node-command.service.js'; import { SEMANTIC_RELEASE_CONFIG_CORE_DEPENDENCIES } from '../constant/semantic-release-config-core-dependencies.constant.js'; import { SEMANTIC_RELEASE_CONFIG_FILE_NAME } from '../constant/semantic-release-config-file-name.constant.js'; import { SEMANTIC_RELEASE_CONFIG_FILE_NAMES } from '../constant/semantic-release-config-file-names.constant.js'; import { SEMANTIC_RELEASE_CONFIG } from '../constant/semantic-release-config.constant.js'; import { PackageJsonService } from './package-json.service.js'; /** * Service for setting up and managing semantic-release configuration. * Provides functionality to automate version management and package publishing * based on commit messages following conventional commits standard. */ class SemanticReleaseModuleService { /** 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 semantic-release configuration */ config = null; /** * Initializes a new instance of the SemanticReleaseModuleService. * @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 semantic-release 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 = ["Existing Semantic Release configuration files detected:"]; messageLines.push(""); if (existingFiles.length > 0) { for (const file of existingFiles) { messageLines.push(`- ${file}`); } } messageLines.push("", "Do you want to delete them?"); 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("Existing Semantic Release configuration files detected. Setup aborted."); return false; } } return true; } /** * Installs and configures semantic-release. * Guides the user through setting up automated versioning and publishing. * @returns Promise resolving to the module setup result */ async install() { try { this.config = await this.CONFIG_SERVICE.getModuleConfig(EModule.SEMANTIC_RELEASE); if (!(await this.shouldInstall())) { return { wasInstalled: false }; } if (!(await this.handleExistingSetup())) { return { wasInstalled: false }; } const setupParameters = await this.setupSemanticRelease(); return { customProperties: setupParameters, wasInstalled: true, }; } catch (error) { this.CLI_INTERFACE_SERVICE.handleError("Failed to complete Semantic Release setup", error); throw error; } } /** * Determines if semantic-release should be installed. * Asks the user if they want to set up automated versioning and publishing. * 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("Do you want to set up Semantic Release for automated versioning and publishing?", await this.CONFIG_SERVICE.isModuleEnabled(EModule.SEMANTIC_RELEASE)); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError("Failed to get user confirmation", error); return false; } } /** * Creates semantic-release configuration files. * Generates the config file with repository URL and branch settings. * @param repositoryUrl - The repository URL for semantic-release * @param mainBranch - The main branch for production releases * @param preReleaseBranch - Optional branch for pre-releases * @param preReleaseChannel - Optional channel name for pre-releases * @param isBackmergeEnabled - Optional flag to enable backmerge to development branch * @param developBranch - Optional development branch name for backmerge */ async createConfigs(repositoryUrl, mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled = false, developBranch) { await this.FILE_SYSTEM_SERVICE.writeFile(SEMANTIC_RELEASE_CONFIG_FILE_NAME, SEMANTIC_RELEASE_CONFIG.template(repositoryUrl, mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled, developBranch), "utf8"); } /** * Displays a summary of the semantic-release setup results. * Lists configured branches, scripts, and usage instructions. * @param mainBranch - The main branch for production releases * @param preReleaseBranch - Optional branch for pre-releases * @param preReleaseChannel - Optional channel name for pre-releases * @param isBackmergeEnabled - Optional flag indicating if backmerge is enabled * @param developBranch - Optional development branch name for backmerge */ displaySetupSummary(mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled = false, developBranch) { const summary = ["Semantic Release configuration has been created.", "", "Release branches:", `- Main release branch: ${mainBranch}`]; if (preReleaseBranch && preReleaseChannel) { summary.push(`- Pre-release branch: ${preReleaseBranch} (channel: ${preReleaseChannel})`); } if (isBackmergeEnabled && developBranch) { summary.push(`- Backmerge enabled: Changes from ${mainBranch} will be automatically merged to ${developBranch} after release`); } summary.push("", "Generated scripts:", "- npm run release", "", "Configuration files:", `- ${SEMANTIC_RELEASE_CONFIG_FILE_NAME}`, "", "Changelog location:", "- CHANGELOG.md", "", "Note: To use Semantic Release effectively, you should:", "1. Configure CI/CD in your repository", "2. Set up required access tokens (GITHUB_TOKEN, NPM_TOKEN)", "3. Use conventional commits (works with the Commitlint setup)"); this.CLI_INTERFACE_SERVICE.note("Semantic Release Setup", summary.join("\n")); } /** * Finds existing semantic-release configuration files. * @returns Promise resolving to an array of file paths for existing configuration files */ async findExistingConfigFiles() { const existingFiles = []; for (const file of SEMANTIC_RELEASE_CONFIG_FILE_NAMES) { if (await this.FILE_SYSTEM_SERVICE.isPathExists(file)) { existingFiles.push(file); } } // Check for CHANGELOG.md in the root directory if (await this.FILE_SYSTEM_SERVICE.isPathExists("CHANGELOG.md")) { existingFiles.push("CHANGELOG.md"); } // Also check for legacy docs/CHANGELOG.md if (await this.FILE_SYSTEM_SERVICE.isPathExists("docs/CHANGELOG.md")) { existingFiles.push("docs/CHANGELOG.md"); } return existingFiles; } /** * Prompts the user for the development branch name for backmerge. * @returns Promise resolving to the development branch name */ async getDevelopBranch() { const initialBranch = this.config?.developBranch ?? "dev"; return await this.CLI_INTERFACE_SERVICE.text("Enter the name of your development branch for backmerge:", "dev", initialBranch, (value) => { if (!value) { return "Branch name is required"; } if (value.includes(" ")) { return "Branch name cannot contain spaces"; } }); } /** * Prompts the user for the main release branch name. * @returns Promise resolving to the main branch name */ async getMainBranch() { const initialBranch = this.config?.mainBranch ?? "main"; return await this.CLI_INTERFACE_SERVICE.text("Enter the name of your main release branch:", "main", initialBranch, (value) => { if (!value) { return "Branch name is required"; } if (value.includes(" ")) { return "Branch name cannot contain spaces"; } }); } /** * Prompts the user for the pre-release branch name. * @returns Promise resolving to the pre-release branch name */ async getPreReleaseBranch() { const initialBranch = this.config?.preReleaseBranch ?? "dev"; return await this.CLI_INTERFACE_SERVICE.text("Enter the name of your pre-release branch:", "dev", initialBranch, (value) => { if (!value) { return "Branch name is required"; } if (value.includes(" ")) { return "Branch name cannot contain spaces"; } }); } /** * Prompts the user for the pre-release channel name. * @returns Promise resolving to the pre-release channel name */ async getPreReleaseChannel() { const initialChannel = this.config?.preReleaseChannel ?? "beta"; return await this.CLI_INTERFACE_SERVICE.text("Enter the pre-release channel name (e.g., beta, alpha, next):", "beta", initialChannel, (value) => { if (!value) { return "Channel name is required"; } if (value.includes(" ")) { return "Channel name cannot contain spaces"; } }); } /** * Gets the repository URL for semantic-release. * Attempts to detect URL from package.json before prompting the user. * @returns Promise resolving to the repository URL */ async getRepositoryUrl() { let savedRepoUrl = this.config?.repositoryUrl ?? ""; if (!savedRepoUrl) { const packageJson = await this.PACKAGE_JSON_SERVICE.get(); if (packageJson.repository) { savedRepoUrl = typeof packageJson.repository === "string" ? packageJson.repository : packageJson.repository.url || ""; } if (savedRepoUrl.startsWith("git+")) { // eslint-disable-next-line @elsikora/typescript/no-magic-numbers savedRepoUrl = savedRepoUrl.slice(4); } if (savedRepoUrl.endsWith(".git")) { // eslint-disable-next-line @elsikora/typescript/no-magic-numbers savedRepoUrl = savedRepoUrl.slice(0, Math.max(0, savedRepoUrl.length - 4)); } } if (savedRepoUrl) { const shouldUseFoundedUrl = await this.CLI_INTERFACE_SERVICE.confirm(`Found repository URL: ${savedRepoUrl}\nIs this correct?`, true); if (!shouldUseFoundedUrl) { savedRepoUrl = await this.CLI_INTERFACE_SERVICE.text("Enter your repository URL (e.g., https://github.com/username/repo):", undefined, savedRepoUrl, (value) => { if (!value) { return "Repository URL is required"; } if (!value.startsWith("https://") && !value.startsWith("http://")) { return "Repository URL must start with 'https://' or 'http://'"; } }); } } else { savedRepoUrl = await this.CLI_INTERFACE_SERVICE.text("Enter your repository URL (e.g., https://github.com/username/repo):", undefined, undefined, (value) => { if (!value) { return "Repository URL is required"; } if (!value.startsWith("https://") && !value.startsWith("http://")) { return "Repository URL must start with 'https://' or 'http://'"; } }); } return savedRepoUrl; } /** * Prompts the user if they want to enable backmerge to development branch. * Only applicable for the main branch. * @param mainBranch - The main branch name * @returns Promise resolving to true if backmerge should be enabled, false otherwise */ async isBackmergeEnabled(mainBranch) { const isConfirmedByDefault = this.config?.isBackmergeEnabled === true; return await this.CLI_INTERFACE_SERVICE.confirm(`Do you want to enable automatic backmerge from ${mainBranch} to development branch after release?`, isConfirmedByDefault); } /** * Prompts the user if they want to enable pre-release channels. * @returns Promise resolving to true if pre-release should be enabled, false otherwise */ async isPrereleaseEnabledChannel() { const isConfirmedByDefault = this.config?.isPrereleaseEnabled === true; return await this.CLI_INTERFACE_SERVICE.confirm("Do you want to configure a pre-release channel for development branches?", isConfirmedByDefault); } /** * Sets up npm scripts for semantic-release. * Adds scripts for running semantic-release and CI processes. */ async setupScripts() { await this.PACKAGE_JSON_SERVICE.addScript("release", "semantic-release"); } /** * Sets up semantic-release configuration. * Collects user input, installs dependencies, creates config files, * and sets up scripts. * @returns Promise resolving to an object containing setup parameters */ async setupSemanticRelease() { try { const parameters = {}; const repositoryUrl = await this.getRepositoryUrl(); parameters.repositoryUrl = repositoryUrl; const mainBranch = await this.getMainBranch(); parameters.mainBranch = mainBranch; const isPrereleaseEnabled = await this.isPrereleaseEnabledChannel(); parameters.isPrereleaseEnabled = isPrereleaseEnabled; let preReleaseBranch = undefined; let preReleaseChannel = undefined; if (isPrereleaseEnabled) { preReleaseBranch = await this.getPreReleaseBranch(); parameters.preReleaseBranch = preReleaseBranch; preReleaseChannel = await this.getPreReleaseChannel(); parameters.preReleaseChannel = preReleaseChannel; } // Backmerge configuration let developBranch = undefined; // Only ask about backmerge if we're not in a pre-release branch const isBackmergeEnabled = await this.isBackmergeEnabled(mainBranch); parameters.isBackmergeEnabled = isBackmergeEnabled; if (isBackmergeEnabled) { developBranch = await this.getDevelopBranch(); parameters.developBranch = developBranch; } this.CLI_INTERFACE_SERVICE.startSpinner("Setting up Semantic Release configuration..."); await this.PACKAGE_JSON_SERVICE.installPackages(SEMANTIC_RELEASE_CONFIG_CORE_DEPENDENCIES, "latest", EPackageJsonDependencyType.DEV); await this.createConfigs(repositoryUrl, mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled, developBranch); await this.setupScripts(); this.CLI_INTERFACE_SERVICE.stopSpinner("Semantic Release configuration completed successfully!"); this.displaySetupSummary(mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled, developBranch); return parameters; } catch (error) { this.CLI_INTERFACE_SERVICE.stopSpinner("Failed to setup Semantic Release configuration"); throw error; } } } export { SemanticReleaseModuleService }; //# sourceMappingURL=semantic-release-module.service.js.map