@elsikora/setup-wizard
Version:
Setup Wizard - CLI scaffolding utility
333 lines (330 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 { NodeCommandService } from '../../infrastructure/service/node-command.service.js';
import { SEMANTIC_RELEASE_CONFIG_CHANGELOG_PATHS } from '../constant/semantic-release/changelog-paths.constant.js';
import { SEMANTIC_RELEASE_CONFIG } from '../constant/semantic-release/config.constant.js';
import { SEMANTIC_RELEASE_CONFIG_CORE_DEPENDENCIES } from '../constant/semantic-release/core-dependencies.constant.js';
import { SEMANTIC_RELEASE_CONFIG_FILE_NAME } from '../constant/semantic-release/file-name.constant.js';
import { SEMANTIC_RELEASE_CONFIG_FILE_NAMES } from '../constant/semantic-release/file-names.constant.js';
import { SEMANTIC_RELEASE_CONFIG_MESSAGES } from '../constant/semantic-release/messages.constant.js';
import { SEMANTIC_RELEASE_CONFIG_SCRIPTS } from '../constant/semantic-release/scripts.constant.js';
import { SEMANTIC_RELEASE_CONFIG_SUMMARY } from '../constant/semantic-release/summary.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 = [SEMANTIC_RELEASE_CONFIG_MESSAGES.existingFilesDetected];
messageLines.push("");
if (existingFiles.length > 0) {
for (const file of existingFiles) {
messageLines.push(`- ${file}`);
}
}
messageLines.push("", SEMANTIC_RELEASE_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(SEMANTIC_RELEASE_CONFIG_MESSAGES.existingFilesAborted);
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(SEMANTIC_RELEASE_CONFIG_MESSAGES.failedSetupError, 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(SEMANTIC_RELEASE_CONFIG_MESSAGES.confirmSetup, await this.CONFIG_SERVICE.isModuleEnabled(EModule.SEMANTIC_RELEASE));
}
catch (error) {
this.CLI_INTERFACE_SERVICE.handleError(SEMANTIC_RELEASE_CONFIG_MESSAGES.failedConfirmation, 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_CONFIG_MESSAGES.configurationCreated, "", SEMANTIC_RELEASE_CONFIG_MESSAGES.releaseBranchesLabel, `${SEMANTIC_RELEASE_CONFIG_MESSAGES.mainReleaseBranchLabel} ${mainBranch}`];
if (preReleaseBranch && preReleaseChannel) {
summary.push(SEMANTIC_RELEASE_CONFIG_MESSAGES.preReleaseBranchLabel(preReleaseBranch, preReleaseChannel));
}
if (isBackmergeEnabled && developBranch) {
summary.push(SEMANTIC_RELEASE_CONFIG_MESSAGES.backmergeEnabledInfo(mainBranch, developBranch));
}
summary.push("", SEMANTIC_RELEASE_CONFIG_MESSAGES.generatedScriptsLabel, SEMANTIC_RELEASE_CONFIG_MESSAGES.releaseScriptDescription, "", SEMANTIC_RELEASE_CONFIG_MESSAGES.configurationFilesLabel, `- ${SEMANTIC_RELEASE_CONFIG_FILE_NAME}`, "", SEMANTIC_RELEASE_CONFIG_MESSAGES.changelogLocationLabel, `- ${SEMANTIC_RELEASE_CONFIG_MESSAGES.changelogLocation}`, "", SEMANTIC_RELEASE_CONFIG_MESSAGES.noteEffectiveUsage, SEMANTIC_RELEASE_CONFIG_MESSAGES.noteInstruction1, SEMANTIC_RELEASE_CONFIG_MESSAGES.noteInstruction2, SEMANTIC_RELEASE_CONFIG_MESSAGES.noteInstruction3);
this.CLI_INTERFACE_SERVICE.note(SEMANTIC_RELEASE_CONFIG_MESSAGES.setupCompleteTitle, 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 paths
for (const changelogPath of SEMANTIC_RELEASE_CONFIG_CHANGELOG_PATHS) {
if (await this.FILE_SYSTEM_SERVICE.isPathExists(changelogPath)) {
existingFiles.push(changelogPath);
}
}
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 ?? SEMANTIC_RELEASE_CONFIG_SUMMARY.devBranchDefault;
return await this.CLI_INTERFACE_SERVICE.text(SEMANTIC_RELEASE_CONFIG_MESSAGES.developBranchPrompt, SEMANTIC_RELEASE_CONFIG_SUMMARY.devBranchDefault, initialBranch, (value) => {
if (!value) {
return SEMANTIC_RELEASE_CONFIG_MESSAGES.branchNameRequired;
}
return value.includes(" ") ? SEMANTIC_RELEASE_CONFIG_MESSAGES.branchNameSpacesError : undefined;
});
}
/**
* Prompts the user for the main release branch name.
* @returns Promise resolving to the main branch name
*/
async getMainBranch() {
const initialBranch = this.config?.mainBranch ?? SEMANTIC_RELEASE_CONFIG_SUMMARY.mainBranchDefault;
return await this.CLI_INTERFACE_SERVICE.text(SEMANTIC_RELEASE_CONFIG_MESSAGES.mainBranchPrompt, SEMANTIC_RELEASE_CONFIG_SUMMARY.mainBranchDefault, initialBranch, (value) => {
if (!value) {
return SEMANTIC_RELEASE_CONFIG_MESSAGES.branchNameRequired;
}
return value.includes(" ") ? SEMANTIC_RELEASE_CONFIG_MESSAGES.branchNameSpacesError : undefined;
});
}
/**
* 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 ?? SEMANTIC_RELEASE_CONFIG_SUMMARY.devBranchDefault;
return await this.CLI_INTERFACE_SERVICE.text(SEMANTIC_RELEASE_CONFIG_MESSAGES.preReleaseBranchPrompt, SEMANTIC_RELEASE_CONFIG_SUMMARY.devBranchDefault, initialBranch, (value) => {
if (!value) {
return SEMANTIC_RELEASE_CONFIG_MESSAGES.branchNameRequired;
}
return value.includes(" ") ? SEMANTIC_RELEASE_CONFIG_MESSAGES.branchNameSpacesError : undefined;
});
}
/**
* 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 ?? SEMANTIC_RELEASE_CONFIG_SUMMARY.preReleaseChannelDefault;
return await this.CLI_INTERFACE_SERVICE.text(SEMANTIC_RELEASE_CONFIG_MESSAGES.preReleaseChannelPrompt, SEMANTIC_RELEASE_CONFIG_SUMMARY.preReleaseChannelDefault, initialChannel, (value) => {
if (!value) {
return SEMANTIC_RELEASE_CONFIG_MESSAGES.channelNameRequired;
}
return value.includes(" ") ? SEMANTIC_RELEASE_CONFIG_MESSAGES.channelNameSpacesError : undefined;
});
}
/**
* 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(SEMANTIC_RELEASE_CONFIG_MESSAGES.foundRepositoryUrl(savedRepoUrl), true);
if (!shouldUseFoundedUrl) {
savedRepoUrl = await this.CLI_INTERFACE_SERVICE.text(SEMANTIC_RELEASE_CONFIG_MESSAGES.enterRepositoryUrl, undefined, savedRepoUrl, (value) => {
if (!value) {
return SEMANTIC_RELEASE_CONFIG_MESSAGES.repositoryUrlRequired;
}
return !value.startsWith("https://") && !value.startsWith("http://") ? SEMANTIC_RELEASE_CONFIG_MESSAGES.repositoryUrlStartError : undefined;
});
}
}
else {
savedRepoUrl = await this.CLI_INTERFACE_SERVICE.text(SEMANTIC_RELEASE_CONFIG_MESSAGES.enterRepositoryUrl, undefined, undefined, (value) => {
if (!value) {
return SEMANTIC_RELEASE_CONFIG_MESSAGES.repositoryUrlRequired;
}
return !value.startsWith("https://") && !value.startsWith("http://") ? SEMANTIC_RELEASE_CONFIG_MESSAGES.repositoryUrlStartError : undefined;
});
}
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(SEMANTIC_RELEASE_CONFIG_MESSAGES.confirmBackmerge(mainBranch), 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(SEMANTIC_RELEASE_CONFIG_MESSAGES.confirmPrereleaseChannel, 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(SEMANTIC_RELEASE_CONFIG_SCRIPTS.release.name, SEMANTIC_RELEASE_CONFIG_SCRIPTS.release.command);
}
/**
* 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(SEMANTIC_RELEASE_CONFIG_MESSAGES.settingUpSpinner);
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_CONFIG_MESSAGES.configurationCompleted);
this.displaySetupSummary(mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled, developBranch);
return parameters;
}
catch (error) {
this.CLI_INTERFACE_SERVICE.stopSpinner(SEMANTIC_RELEASE_CONFIG_MESSAGES.failedSetupConfiguration);
throw error;
}
}
}
export { SemanticReleaseModuleService };
//# sourceMappingURL=semantic-release-module.service.js.map